feat: complete kiro platform support
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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(() => [
|
||||
|
||||
Reference in New Issue
Block a user