feat: complete kiro platform support

This commit is contained in:
nianzs
2026-04-29 18:20:46 +08:00
parent fcaa8ea86a
commit b09bcb6a3c
39 changed files with 2063 additions and 164 deletions
+7
View File
@@ -558,6 +558,13 @@ export async function getAntigravityDefaultModelMapping(): Promise<Record<string
return data
}
export async function getKiroDefaultModelMapping(): Promise<Record<string, string>> {
const { data } = await apiClient.get<Record<string, string>>(
'/admin/accounts/kiro/default-model-mapping'
)
return data
}
/**
* Refresh OpenAI token using refresh token
* @param refreshToken - The refresh token
@@ -13,6 +13,15 @@ vi.mock('vue-i18n', async () => {
}
})
vi.mock('@/i18n', () => ({
i18n: {
global: {
t: (key: string) => key
}
},
getLocale: () => 'en'
}))
function makeAccount(overrides: Partial<Account>): Account {
return {
id: 1,
@@ -35,6 +44,12 @@ function makeAccount(overrides: Partial<Account>): Account {
overload_until: null,
temp_unschedulable_until: null,
temp_unschedulable_reason: null,
kiro_quota_state: null,
kiro_quota_reason: null,
kiro_quota_reset_at: null,
kiro_runtime_state: null,
kiro_runtime_reason: null,
kiro_runtime_reset_at: null,
session_window_start: null,
session_window_end: null,
session_window_status: null,
@@ -159,4 +174,96 @@ describe('AccountStatusIndicator', () => {
// AICredits 积分耗尽状态应显示
expect(wrapper.text()).toContain('admin.accounts.status.creditsExhausted')
})
it('Kiro 运行时冷却在状态列复用限流展示', () => {
const wrapper = mount(AccountStatusIndicator, {
props: {
account: makeAccount({
id: 5,
name: 'kiro-cooldown',
platform: 'kiro',
kiro_runtime_state: 'cooldown',
kiro_runtime_reason: 'rate_limit_exceeded',
kiro_runtime_reset_at: '2099-03-15T00:00:00Z'
})
},
global: {
stubs: {
Icon: true
}
}
})
expect(wrapper.text()).toContain('admin.accounts.status.rateLimited')
expect(wrapper.text()).toContain('admin.accounts.status.rateLimitedAutoResume')
expect(wrapper.text()).toContain('429')
})
it('Kiro suspended 在状态列显示为 forbidden', () => {
const wrapper = mount(AccountStatusIndicator, {
props: {
account: makeAccount({
id: 6,
name: 'kiro-suspended',
platform: 'kiro',
kiro_runtime_state: 'suspended',
kiro_runtime_reason: 'account_suspended',
kiro_runtime_reset_at: '2099-03-15T00:00:00Z'
})
},
global: {
stubs: {
Icon: true
}
}
})
expect(wrapper.text()).toContain('admin.accounts.forbidden')
})
it('Kiro overage active 在状态列仍显示正常状态', () => {
const wrapper = mount(AccountStatusIndicator, {
props: {
account: makeAccount({
id: 7,
name: 'kiro-overage-active',
platform: 'kiro',
kiro_quota_state: 'overage_active',
kiro_quota_reason: 'overages_enabled',
kiro_quota_reset_at: '2099-03-15T00:00:00Z'
})
},
global: {
stubs: {
Icon: true
}
}
})
expect(wrapper.text()).toContain('admin.accounts.status.active')
expect(wrapper.text()).not.toContain('admin.accounts.status.overageActive')
})
it('Kiro overage exhausted 在状态列显示危险徽章', () => {
const wrapper = mount(AccountStatusIndicator, {
props: {
account: makeAccount({
id: 8,
name: 'kiro-overage-exhausted',
platform: 'kiro',
kiro_quota_state: 'overage_exhausted',
kiro_quota_reason: 'overage disabled after quota exhaustion',
kiro_quota_reset_at: '2099-03-15T00:00:00Z'
})
},
global: {
stubs: {
Icon: true
}
}
})
expect(wrapper.text()).toContain('admin.accounts.status.overageExhausted')
expect(wrapper.text()).toContain('admin.accounts.status.overageExhaustedUntil')
})
})
@@ -25,6 +25,15 @@ vi.mock('vue-i18n', async () => {
}
})
vi.mock('@/i18n', () => ({
i18n: {
global: {
t: (key: string) => key
}
},
getLocale: () => 'en'
}))
function makeAccount(overrides: Partial<Account>): Account {
return {
id: 1,
@@ -47,6 +56,12 @@ function makeAccount(overrides: Partial<Account>): Account {
overload_until: null,
temp_unschedulable_until: null,
temp_unschedulable_reason: null,
kiro_quota_state: null,
kiro_quota_reason: null,
kiro_quota_reset_at: null,
kiro_runtime_state: null,
kiro_runtime_reason: null,
kiro_runtime_reset_at: null,
session_window_start: null,
session_window_end: null,
session_window_status: null,
@@ -530,6 +545,234 @@ describe('AccountUsageCell', () => {
expect(wrapper.text()).toContain('7d|100|106540000')
})
it('Kiro OAuth 会用 passive source 拉取并展示 credits 额度', async () => {
const account = makeAccount({
id: 3001,
platform: 'kiro',
type: 'oauth',
extra: {},
credentials: {}
})
getUsage.mockResolvedValue({
source: 'passive',
kiro_subscription_name: 'KIRO PRO+',
kiro_overages_enabled: true,
kiro_credit: {
current_usage: 125,
usage_limit: 2000,
percentage_used: 6.25,
},
kiro_bonus: {
current_usage: 25,
usage_limit: 500,
percentage_used: 5,
days_remaining: 7,
},
kiro_overage: {
current_overages: 2,
overage_charges: 0.08,
currency_symbol: '$',
currency_code: 'USD',
},
kiro_reset_at: '2099-03-13T12:00:00Z',
})
const wrapper = mount(AccountUsageCell, {
props: {
account
},
global: {
stubs: {
UsageProgressBar: true,
AccountQuotaInfo: true
}
}
})
await flushPromises()
expect(getUsage).toHaveBeenCalledWith(3001, 'passive')
expect(wrapper.emitted('kiroUsageMeta')?.[0]).toEqual([
{
plan_type: 'KIRO PRO+',
kiro_overages_enabled: true
}
])
expect(wrapper.text()).toContain('admin.accounts.usageWindow.kiroCredits')
expect(wrapper.text()).toContain('125 / 2.0K')
expect(wrapper.text()).toContain('admin.accounts.usageWindow.kiroBonus')
expect(wrapper.text()).toContain('25 / 500')
expect(wrapper.text()).toContain('admin.accounts.usageWindow.kiroDaysLeft')
expect(wrapper.text()).toContain('admin.accounts.usageWindow.kiroReset')
expect(wrapper.text()).toContain('admin.accounts.usageWindow.kiroOverage 2 ($0.08)')
})
it('Kiro OAuth 会展示运行时冷却状态', async () => {
getUsage.mockResolvedValue({
source: 'passive',
kiro_runtime_state: 'cooldown',
kiro_runtime_reason: 'rate_limit_exceeded',
kiro_runtime_reset_at: '2099-03-13T12:00:00Z',
kiro_credit: {
current_usage: 10,
usage_limit: 100,
percentage_used: 10,
},
})
const wrapper = mount(AccountUsageCell, {
props: {
account: makeAccount({
id: 3002,
platform: 'kiro',
type: 'oauth',
extra: {},
credentials: {}
})
},
global: {
stubs: {
UsageProgressBar: true,
AccountQuotaInfo: true
}
}
})
await flushPromises()
expect(wrapper.text()).toContain('admin.accounts.status.rateLimited')
expect(wrapper.text()).toContain('admin.accounts.status.rateLimitedUntil')
})
it('Kiro OAuth 会展示 overage active 与 exhausted 状态', async () => {
getUsage.mockResolvedValueOnce({
source: 'passive',
kiro_quota_state: 'overage_active',
kiro_quota_reason: 'overages_enabled',
kiro_quota_reset_at: '2099-03-13T12:00:00Z',
kiro_overages_enabled: true,
kiro_credit: {
current_usage: 2100,
usage_limit: 2000,
percentage_used: 100,
},
kiro_overage: {
current_overages: 3,
overage_charges: 0.12,
currency_symbol: '$',
},
})
const activeWrapper = mount(AccountUsageCell, {
props: {
account: makeAccount({
id: 3005,
platform: 'kiro',
type: 'oauth',
extra: {},
credentials: {}
})
},
global: {
stubs: {
UsageProgressBar: true,
AccountQuotaInfo: true
}
}
})
await flushPromises()
expect(activeWrapper.text()).toContain('admin.accounts.status.overageActive')
expect(activeWrapper.text()).not.toContain('admin.accounts.status.overageActiveUntil')
getUsage.mockResolvedValueOnce({
source: 'passive',
kiro_quota_state: 'overage_exhausted',
kiro_quota_reason: 'usage API error: overage exhausted',
kiro_quota_reset_at: '2099-03-13T12:00:00Z',
error: 'usage API error: kiro usage request failed (status 429): {"message":"overage exhausted"}',
})
const exhaustedWrapper = mount(AccountUsageCell, {
props: {
account: makeAccount({
id: 3006,
platform: 'kiro',
type: 'oauth',
extra: {},
credentials: {}
})
},
global: {
stubs: {
UsageProgressBar: true,
AccountQuotaInfo: true
}
}
})
await flushPromises()
expect(exhaustedWrapper.text()).toContain('admin.accounts.status.overageExhausted')
expect(exhaustedWrapper.text()).toContain('admin.accounts.status.overageExhaustedUntil')
})
it('Kiro OAuth 会展示 profile 异常和 usage forbidden 徽章', async () => {
getUsage.mockResolvedValueOnce({
source: 'passive',
error_code: 'forbidden',
error: 'usage API error: kiro usage request failed (status 400): {"message":"profileArn is required for this request."}',
})
const profileWrapper = mount(AccountUsageCell, {
props: {
account: makeAccount({
id: 3003,
platform: 'kiro',
type: 'oauth',
extra: {},
credentials: {}
})
},
global: {
stubs: {
UsageProgressBar: true,
AccountQuotaInfo: true
}
}
})
await flushPromises()
expect(profileWrapper.text()).toContain('admin.accounts.usageError')
getUsage.mockResolvedValueOnce({
source: 'passive',
error_code: 'forbidden',
error: 'usage API error: kiro usage request failed (status 403): {"message":"User is not authorized to access this feature."}',
})
const forbiddenWrapper = mount(AccountUsageCell, {
props: {
account: makeAccount({
id: 3004,
platform: 'kiro',
type: 'oauth',
extra: {},
credentials: {}
})
},
global: {
stubs: {
UsageProgressBar: true,
AccountQuotaInfo: true
}
}
})
await flushPromises()
expect(forbiddenWrapper.text()).toContain('admin.accounts.forbidden')
})
it('Key 账号会展示 today stats 徽章并带 A/U 提示', async () => {
const wrapper = mount(AccountUsageCell, {
props: {
@@ -0,0 +1,56 @@
import { describe, expect, it, vi } from 'vitest'
import { nextTick } from 'vue'
import { mount } from '@vue/test-utils'
vi.mock('@/stores/app', () => ({
useAppStore: () => ({
showSuccess: vi.fn(),
showError: vi.fn()
})
}))
vi.mock('@/composables/useClipboard', () => ({
useClipboard: () => ({
copied: { value: false },
copyToClipboard: vi.fn()
})
}))
vi.mock('vue-i18n', async () => {
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
return {
...actual,
useI18n: () => ({
t: (key: string) => key
})
}
})
import OAuthAuthorizationFlow from '../OAuthAuthorizationFlow.vue'
describe('OAuthAuthorizationFlow', () => {
it('extracts code, state, and callback metadata from a full Kiro callback URL', async () => {
const wrapper = mount(OAuthAuthorizationFlow, {
props: {
addMethod: 'oauth',
platform: 'kiro',
authUrl: 'https://example.com/authorize',
sessionId: 'session-1'
},
global: {
stubs: {
Icon: true
}
}
})
const textarea = wrapper.get('textarea')
await textarea.setValue('http://localhost:49153/oauth/callback?code=abc123&state=state456&login_option=github')
await nextTick()
expect((textarea.element as HTMLTextAreaElement).value).toBe('abc123')
expect((wrapper.vm as any).oauthState).toBe('state456')
expect((wrapper.vm as any).oauthCallbackPath).toBe('/oauth/callback')
expect((wrapper.vm as any).oauthLoginOption).toBe('github')
})
})
@@ -489,7 +489,8 @@ const platformOptions = [
{ value: 'anthropic', label: 'Anthropic' },
{ value: 'openai', label: 'OpenAI' },
{ value: 'gemini', label: 'Gemini' },
{ value: 'antigravity', label: 'Antigravity' }
{ value: 'antigravity', label: 'Antigravity' },
{ value: 'kiro', label: 'Kiro' }
]
// Load rules when dialog opens
@@ -115,7 +115,7 @@ const labelClass = computed(() => {
}
// 正常状态或无天数:根据平台显示主题色
if (props.platform === 'anthropic') {
if (props.platform === 'anthropic' || props.platform === 'kiro') {
return `${base} bg-orange-200/60 text-orange-800 dark:bg-orange-800/40 dark:text-orange-300`
}
if (props.platform === 'openai') {
@@ -129,7 +129,7 @@ const labelClass = computed(() => {
// Badge color based on platform and subscription type
const badgeClass = computed(() => {
if (props.platform === 'anthropic') {
if (props.platform === 'anthropic' || props.platform === 'kiro') {
// Claude: orange theme
return isSubscription.value
? 'bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-400'
@@ -91,6 +91,8 @@ const ratePillClass = computed(() => {
return 'bg-green-50 text-green-700 dark:bg-green-900/20 dark:text-green-400'
case 'gemini':
return 'bg-sky-50 text-sky-700 dark:bg-sky-900/20 dark:text-sky-400'
case 'kiro':
return 'bg-amber-50 text-amber-700 dark:bg-amber-900/20 dark:text-amber-400'
default: // antigravity and others
return 'bg-violet-50 text-violet-700 dark:bg-violet-900/20 dark:text-violet-400'
}
@@ -0,0 +1,42 @@
import { mount } from '@vue/test-utils'
import { describe, expect, it, vi } from 'vitest'
import PlatformTypeBadge from '../PlatformTypeBadge.vue'
vi.mock('vue-i18n', async () => {
const actual = await vi.importActual<typeof import('vue-i18n')>('vue-i18n')
return {
...actual,
useI18n: () => ({
t: (key: string) => key === 'admin.accounts.status.overageActive' ? 'Overage' : key
})
}
})
describe('PlatformTypeBadge', () => {
it('shows Kiro overages tag next to the plan tag when enabled', () => {
const wrapper = mount(PlatformTypeBadge, {
props: {
platform: 'kiro',
type: 'oauth',
planType: 'KIRO PRO+',
overagesEnabled: true
}
})
expect(wrapper.text()).toContain('KIRO PRO+')
expect(wrapper.text()).toContain('Overage')
})
it('does not show overages tag for non-Kiro accounts', () => {
const wrapper = mount(PlatformTypeBadge, {
props: {
platform: 'openai',
type: 'oauth',
planType: 'Pro',
overagesEnabled: true
}
})
expect(wrapper.text()).not.toContain('Overage')
})
})
@@ -4,7 +4,12 @@ vi.mock('@/api/admin/accounts', () => ({
getAntigravityDefaultModelMapping: vi.fn()
}))
import { buildModelMappingObject, getModelsByPlatform } from '../useModelWhitelist'
import {
buildModelMappingObject,
fetchKiroDefaultMappings,
getModelsByPlatform,
getPresetMappingsByPlatform
} from '../useModelWhitelist'
describe('useModelWhitelist', () => {
it('openai 模型列表包含 GPT-5.4 官方快照', () => {
@@ -59,6 +64,49 @@ describe('useModelWhitelist', () => {
expect(models.every((model) => !model.endsWith('-agentic') && !model.endsWith('-chat'))).toBe(true)
})
it('kiro 模型列表只保留 Claude 模型', () => {
const models = getModelsByPlatform('kiro')
expect(models).toEqual([
'claude-opus-4-6',
'claude-opus-4-6-thinking',
'claude-sonnet-4-6',
'claude-sonnet-4-6-thinking',
'claude-opus-4-5-20251101',
'claude-opus-4-5-20251101-thinking',
'claude-sonnet-4-5-20250929',
'claude-sonnet-4-5-20250929-thinking',
'claude-haiku-4-5-20251001',
'claude-haiku-4-5-20251001-thinking'
])
expect(models.every(model => model.startsWith('claude-'))).toBe(true)
expect(models.some(model => model.endsWith('-agentic'))).toBe(false)
expect(models.some(model => model.endsWith('-chat'))).toBe(false)
expect(models).not.toContain('kiro-auto')
expect(models).not.toContain('claude-opus-4-5')
expect(models).not.toContain('claude-sonnet-4-5')
expect(models).not.toContain('claude-sonnet-4')
expect(models).not.toContain('claude-3-5-sonnet-20241022')
expect(models).not.toContain('claude-3-5-haiku-20241022')
expect(models).not.toContain('claude-haiku-4-5')
expect(models).not.toContain('gpt-4o')
expect(models).not.toContain('gpt-4')
expect(models).not.toContain('gpt-4-turbo')
expect(models).not.toContain('gpt-3.5-turbo')
expect(models).not.toContain('deepseek-3-2')
expect(models).not.toContain('minimax-m2-1')
expect(models).not.toContain('qwen3-coder-next')
})
it('claude 模型列表包含 dated 和 thinking 兼容别名', () => {
const models = getModelsByPlatform('claude')
expect(models).toContain('claude-opus-4-6-thinking')
expect(models).toContain('claude-opus-4-5-20251101-thinking')
expect(models).toContain('claude-sonnet-4-20250514-thinking')
expect(models).toContain('claude-haiku-4-5-20251001-thinking')
})
it('whitelist 模式会忽略通配符条目', () => {
const mapping = buildModelMappingObject('whitelist', ['claude-*', 'gemini-3.1-flash-image'], [])
expect(mapping).toEqual({
@@ -81,4 +129,68 @@ describe('useModelWhitelist', () => {
'gpt-5.4-mini': 'gpt-5.4-mini'
})
})
it('kiro 预设映射只暴露 Claude 入口', () => {
const mappings = getPresetMappingsByPlatform('kiro')
const mappingTargets = mappings.map(item => item.to)
expect(mappings.map(({ from, to }) => ({ from, to }))).toEqual([
{ from: 'claude-opus-4-6', to: 'claude-opus-4.6' },
{ from: 'claude-opus-4-6-thinking', to: 'claude-opus-4.6' },
{ from: 'claude-sonnet-4-6', to: 'claude-sonnet-4.6' },
{ from: 'claude-sonnet-4-6-thinking', to: 'claude-sonnet-4.6' },
{ from: 'claude-opus-4-5-20251101', to: 'claude-opus-4.5' },
{ from: 'claude-opus-4-5-20251101-thinking', to: 'claude-opus-4.5' },
{ from: 'claude-sonnet-4-5-20250929', to: 'claude-sonnet-4.5' },
{ from: 'claude-sonnet-4-5-20250929-thinking', to: 'claude-sonnet-4.5' },
{ from: 'claude-haiku-4-5-20251001', to: 'claude-haiku-4.5' },
{ from: 'claude-haiku-4-5-20251001-thinking', to: 'claude-haiku-4.5' }
])
expect(mappings.every(item => item.from.startsWith('claude-'))).toBe(true)
expect(mappingTargets.every(model => model.startsWith('claude-'))).toBe(true)
expect(mappingTargets.some(model => model.endsWith('-agentic'))).toBe(false)
expect(mappingTargets.some(model => model.endsWith('-chat'))).toBe(false)
expect(mappingTargets).not.toContain('kiro-auto')
expect(mappingTargets.some(model => model.startsWith('kiro-'))).toBe(false)
expect(mappings.some(item => item.from === 'claude-opus-4-5')).toBe(false)
expect(mappings.some(item => item.from === 'claude-sonnet-4-5')).toBe(false)
expect(mappings.some(item => item.from === 'claude-sonnet-4')).toBe(false)
expect(mappings.some(item => item.from === 'claude-3-5-sonnet-20241022')).toBe(false)
expect(mappings.some(item => item.from === 'claude-3-5-haiku-20241022')).toBe(false)
expect(mappings.some(item => item.from === 'claude-haiku-4-5')).toBe(false)
expect(mappingTargets).not.toContain('gpt-4o')
expect(mappingTargets).not.toContain('gpt-4')
expect(mappingTargets).not.toContain('gpt-4-turbo')
expect(mappingTargets).not.toContain('gpt-3.5-turbo')
expect(mappingTargets).not.toContain('deepseek-3.2')
expect(mappingTargets).not.toContain('minimax-m2.1')
expect(mappingTargets).not.toContain('qwen3-coder-next')
})
it('kiro 默认映射会在前端填充所有可精确定价模型', async () => {
const mappings = await fetchKiroDefaultMappings()
expect(mappings).toEqual(expect.arrayContaining([
{ from: 'claude-opus-4-6', to: 'claude-opus-4.6' },
{ from: 'claude-opus-4-6-thinking', to: 'claude-opus-4.6' },
{ from: 'claude-sonnet-4-6', to: 'claude-sonnet-4.6' },
{ from: 'claude-sonnet-4-6-thinking', to: 'claude-sonnet-4.6' },
{ from: 'claude-opus-4-5-20251101', to: 'claude-opus-4.5' },
{ from: 'claude-opus-4-5-20251101-thinking', to: 'claude-opus-4.5' },
{ from: 'claude-sonnet-4-5-20250929', to: 'claude-sonnet-4.5' },
{ from: 'claude-sonnet-4-5-20250929-thinking', to: 'claude-sonnet-4.5' },
{ from: 'claude-haiku-4-5-20251001', to: 'claude-haiku-4.5' },
{ from: 'claude-haiku-4-5-20251001-thinking', to: 'claude-haiku-4.5' }
]))
expect(mappings).toHaveLength(10)
expect(mappings.every(item => !item.from.startsWith('kiro-'))).toBe(true)
expect(mappings.every(item => !item.to.startsWith('kiro-'))).toBe(true)
expect(mappings.every(item => !item.from.endsWith('-agentic'))).toBe(true)
expect(mappings.every(item => !item.to.endsWith('-agentic'))).toBe(true)
expect(mappings.every(item => !item.from.endsWith('-chat'))).toBe(true)
expect(mappings.every(item => !item.to.endsWith('-chat'))).toBe(true)
expect(mappings.every(item => item.from.startsWith('claude-'))).toBe(true)
expect(mappings.every(item => item.to.startsWith('claude-'))).toBe(true)
expect(mappings.some(item => item.to === 'claude-opus-4-7')).toBe(false)
})
})
@@ -23,13 +23,21 @@ export const claudeModels = [
'claude-3-5-sonnet-20241022', 'claude-3-5-sonnet-20240620',
'claude-3-5-haiku-20241022',
'claude-3-7-sonnet-20250219',
'claude-sonnet-4-20250514-thinking', 'claude-opus-4-20250514-thinking',
'claude-sonnet-4-20250514', 'claude-opus-4-20250514',
'claude-opus-4-1-20250805',
'claude-sonnet-4-5-thinking', 'claude-sonnet-4-5-20250929-thinking',
'claude-haiku-4-5-thinking', 'claude-haiku-4-5-20251001-thinking',
'claude-opus-4-5-thinking', 'claude-opus-4-5-20251101-thinking',
'claude-sonnet-4-5-20250929', 'claude-haiku-4-5-20251001',
'claude-opus-4-5-20251101',
'claude-opus-4-6-thinking',
'claude-opus-4-6',
'claude-opus-4-7-thinking',
'claude-opus-4-7',
'claude-sonnet-4-6'
'claude-sonnet-4-6-thinking',
'claude-sonnet-4-6',
'claude-2.1', 'claude-2.0', 'claude-instant-1.2'
]
// Google Gemini
+8 -4
View File
@@ -324,8 +324,8 @@
</div>
</div>
<!-- Web Search Emulation (Anthropic only, hidden when global disabled) -->
<div v-if="section.platform === 'anthropic' && webSearchGlobalEnabled" class="border-t border-gray-200 pt-3 dark:border-dark-600">
<!-- Web Search Emulation (supported platforms only, hidden when global disabled) -->
<div v-if="supportsWebSearchEmulation(section.platform) && webSearchGlobalEnabled" class="border-t border-gray-200 pt-3 dark:border-dark-600">
<div class="flex items-center justify-between">
<div>
<label class="text-xs font-medium text-gray-700 dark:text-gray-300">
@@ -718,7 +718,7 @@ const form = reactive({
let abortController: AbortController | null = null
// ── Platform config ──
const platformOrder: GroupPlatform[] = ['anthropic', 'openai', 'gemini', 'antigravity']
const platformOrder: GroupPlatform[] = ['anthropic', 'openai', 'gemini', 'antigravity', 'kiro']
// ── Helpers ──
function formatDate(value: string): string {
@@ -758,6 +758,10 @@ function getGroupsForPlatform(platform: GroupPlatform): AdminGroup[] {
return allGroups.value.filter(g => g.platform === platform)
}
function supportsWebSearchEmulation(platform: GroupPlatform): boolean {
return platform === 'anthropic' || platform === 'kiro'
}
// ── Group helpers ──
const groupToChannelMap = computed(() => {
const map = new Map<number, Channel>()
@@ -1037,7 +1041,7 @@ function formToAPI(): { group_ids: number[], model_pricing: ChannelModelPricing[
const wsEmulation: Record<string, boolean> = {}
for (const section of form.platforms) {
if (!section.enabled) continue
if (section.platform === 'anthropic') {
if (supportsWebSearchEmulation(section.platform)) {
wsEmulation[section.platform] = !!section.web_search_emulation
}
}
@@ -967,7 +967,8 @@ const platformFilterOptions = computed(() => [
{ value: 'anthropic', label: 'Anthropic' },
{ value: 'openai', label: 'OpenAI' },
{ value: 'gemini', label: 'Gemini' },
{ value: 'antigravity', label: 'Antigravity' }
{ value: 'antigravity', label: 'Antigravity' },
{ value: 'kiro', label: 'Kiro' }
])
// Group options for assign (only subscription type groups)
@@ -111,7 +111,8 @@ const platformOptions = computed(() => [
{ value: 'openai', label: 'OpenAI' },
{ value: 'anthropic', label: 'Anthropic' },
{ value: 'gemini', label: 'Gemini' },
{ value: 'antigravity', label: 'Antigravity' }
{ value: 'antigravity', label: 'Antigravity' },
{ value: 'kiro', label: 'Kiro' }
])
const timeRangeOptions = computed(() => [