@@ -2145,6 +2310,7 @@ import {
resolveOpenAIWSModeFromExtra
} from '@/utils/openaiWsMode'
import {
+ fetchKiroDefaultMappings,
getPresetMappingsByPlatform,
commonErrorCodes,
buildModelMappingObject,
@@ -2173,11 +2339,13 @@ const baseUrlHint = computed(() => {
if (!props.account) return t('admin.accounts.baseUrlHint')
if (props.account.platform === 'openai') return t('admin.accounts.openai.baseUrlHint')
if (props.account.platform === 'gemini') return t('admin.accounts.gemini.baseUrlHint')
+ if (props.account.platform === 'kiro') return t('admin.accounts.kiro.baseUrlHint')
return t('admin.accounts.baseUrlHint')
})
const antigravityPresetMappings = computed(() => getPresetMappingsByPlatform('antigravity'))
const bedrockPresets = computed(() => getPresetMappingsByPlatform('bedrock'))
+const isKiroOAuthAccount = computed(() => props.account?.platform === 'kiro' && props.account?.type === 'oauth')
// Model mapping type
interface ModelMapping {
@@ -2235,6 +2403,21 @@ const getOpenAICompactModelMappingKey = createStableObjectKeyResolver
('edit-antigravity-model-mapping')
const getTempUnschedRuleKey = createStableObjectKeyResolver('edit-temp-unsched-rule')
+const applyKiroModelMappings = (entries: Array<[string, string]>) => {
+ modelRestrictionMode.value = 'mapping'
+ modelMappings.value = entries.map(([from, to]) => ({ from, to }))
+ allowedModels.value = []
+}
+
+const loadDefaultKiroModelMappings = () => {
+ fetchKiroDefaultMappings().then(mappings => {
+ if (!isKiroOAuthAccount.value) return
+ modelRestrictionMode.value = 'mapping'
+ modelMappings.value = mappings.map(({ from, to }) => ({ from, to }))
+ allowedModels.value = []
+ })
+}
+
const showMixedChannelWarning = ref(false)
const mixedChannelWarningDetails = ref<{ groupName: string; currentPlatform: string; otherPlatform: string } | null>(
null
@@ -2383,6 +2566,7 @@ const tempUnschedPresets = computed(() => [
const defaultBaseUrl = computed(() => {
if (props.account?.platform === 'openai') return 'https://api.openai.com'
if (props.account?.platform === 'gemini') return 'https://generativelanguage.googleapis.com'
+ if (props.account?.platform === 'kiro') return ''
return 'https://api.anthropic.com'
})
@@ -2597,6 +2781,8 @@ const syncFormFromAccount = (newAccount: Account | null) => {
? 'https://api.openai.com'
: newAccount.platform === 'gemini'
? 'https://generativelanguage.googleapis.com'
+ : newAccount.platform === 'kiro'
+ ? ''
: 'https://api.anthropic.com'
editBaseUrl.value = (credentials.base_url as string) || platformDefaultUrl
@@ -2605,20 +2791,35 @@ const syncFormFromAccount = (newAccount: Account | null) => {
if (existingMappings && typeof existingMappings === 'object') {
const entries = Object.entries(existingMappings)
- // Detect if this is whitelist mode (all from === to) or mapping mode
- const isWhitelistMode = entries.length > 0 && entries.every(([from, to]) => from === to)
-
- if (isWhitelistMode) {
- // Whitelist mode: populate allowedModels
- modelRestrictionMode.value = 'whitelist'
- allowedModels.value = entries.map(([from]) => from)
- modelMappings.value = []
- } else {
- // Mapping mode: populate modelMappings
+ if (newAccount.platform === 'kiro') {
modelRestrictionMode.value = 'mapping'
modelMappings.value = entries.map(([from, to]) => ({ from, to }))
allowedModels.value = []
+ } else {
+ // Detect if this is whitelist mode (all from === to) or mapping mode
+ const isWhitelistMode = entries.length > 0 && entries.every(([from, to]) => from === to)
+
+ if (isWhitelistMode) {
+ // Whitelist mode: populate allowedModels
+ modelRestrictionMode.value = 'whitelist'
+ allowedModels.value = entries.map(([from]) => from)
+ modelMappings.value = []
+ } else {
+ // Mapping mode: populate modelMappings
+ modelRestrictionMode.value = 'mapping'
+ modelMappings.value = entries.map(([from, to]) => ({ from, to }))
+ allowedModels.value = []
+ }
}
+ } else if (newAccount.platform === 'kiro') {
+ fetchKiroDefaultMappings().then(mappings => {
+ if (props.account?.id !== newAccount.id || props.account?.type !== 'apikey' || props.account?.platform !== 'kiro') {
+ return
+ }
+ modelRestrictionMode.value = 'mapping'
+ modelMappings.value = mappings.map(({ from, to }) => ({ from, to }))
+ allowedModels.value = []
+ })
} else {
// No mappings: default to whitelist mode with empty selection (allow all)
modelRestrictionMode.value = 'whitelist'
@@ -2673,15 +2874,18 @@ const syncFormFromAccount = (newAccount: Account | null) => {
const entries = Object.entries(existingMappings)
const isWhitelistMode = entries.length > 0 && entries.every(([from, to]) => from === to)
if (isWhitelistMode) {
+ // Whitelist mode: populate allowedModels
modelRestrictionMode.value = 'whitelist'
allowedModels.value = entries.map(([from]) => from)
modelMappings.value = []
} else {
+ // Mapping mode: populate modelMappings
modelRestrictionMode.value = 'mapping'
modelMappings.value = entries.map(([from, to]) => ({ from, to }))
allowedModels.value = []
}
} else {
+ // No mappings: default to whitelist mode with empty selection (allow all)
modelRestrictionMode.value = 'whitelist'
modelMappings.value = []
allowedModels.value = []
@@ -2723,8 +2927,16 @@ const syncFormFromAccount = (newAccount: Account | null) => {
: 'https://api.anthropic.com'
editBaseUrl.value = platformDefaultUrl
- // Load model mappings for OpenAI OAuth accounts
- if (newAccount.platform === 'openai' && newAccount.credentials) {
+ // Load model mappings for OpenAI/Kiro OAuth accounts
+ if (newAccount.platform === 'kiro' && newAccount.credentials) {
+ const oauthCredentials = newAccount.credentials as Record
+ const existingMappings = oauthCredentials.model_mapping as Record | undefined
+ if (existingMappings && typeof existingMappings === 'object' && Object.keys(existingMappings).length > 0) {
+ applyKiroModelMappings(Object.entries(existingMappings))
+ } else {
+ loadDefaultKiroModelMappings()
+ }
+ } else if (newAccount.platform === 'openai' && newAccount.credentials) {
const oauthCredentials = newAccount.credentials as Record
const existingMappings = oauthCredentials.model_mapping as Record | undefined
if (existingMappings && typeof existingMappings === 'object') {
@@ -2780,6 +2992,7 @@ watch(
{ immediate: true }
)
+
// Model mapping helpers
const addModelMapping = () => {
modelMappings.value.push({ from: '', to: '' })
@@ -3221,9 +3434,16 @@ const handleSubmit = async () => {
// For apikey type, handle credentials update
if (props.account.type === 'apikey') {
const currentCredentials = (props.account.credentials as Record) || {}
- const newBaseUrl = editBaseUrl.value.trim() || defaultBaseUrl.value
+ const newBaseUrl = props.account.platform === 'kiro'
+ ? editBaseUrl.value.trim()
+ : (editBaseUrl.value.trim() || defaultBaseUrl.value)
const shouldApplyModelMapping = !(props.account.platform === 'openai' && openaiPassthroughEnabled.value)
+ if (!newBaseUrl) {
+ appStore.showError(t('admin.accounts.upstream.pleaseEnterBaseUrl'))
+ return
+ }
+
// Always update credentials for apikey type to handle model mapping changes
const newCredentials: Record = {
...currentCredentials,
@@ -3244,7 +3464,11 @@ const handleSubmit = async () => {
// Add model mapping if configured(OpenAI 开启自动透传时保留现有映射,不再编辑)
if (shouldApplyModelMapping) {
- const modelMapping = buildModelMappingObject(modelRestrictionMode.value, allowedModels.value, modelMappings.value)
+ const modelMapping = buildModelMappingObject(
+ props.account.platform === 'kiro' ? 'mapping' : modelRestrictionMode.value,
+ props.account.platform === 'kiro' ? [] : allowedModels.value,
+ modelMappings.value
+ )
if (modelMapping) {
newCredentials.model_mapping = modelMapping
} else {
@@ -3382,7 +3606,7 @@ const handleSubmit = async () => {
}
// Model mapping
- const modelMapping = buildModelMappingObject(modelRestrictionMode.value, allowedModels.value, modelMappings.value)
+ const modelMapping = buildModelMappingObject('mapping', [], modelMappings.value)
if (modelMapping) {
newCredentials.model_mapping = modelMapping
} else {
@@ -3436,6 +3660,22 @@ const handleSubmit = async () => {
updatePayload.credentials = newCredentials
}
+ // Kiro OAuth: persist model mapping to credentials
+ if (props.account.platform === 'kiro' && props.account.type === 'oauth') {
+ const currentCredentials = (updatePayload.credentials as Record) ||
+ ((props.account.credentials as Record) || {})
+ const newCredentials: Record = { ...currentCredentials }
+
+ const modelMapping = buildModelMappingObject('mapping', [], modelMappings.value)
+ if (modelMapping) {
+ newCredentials.model_mapping = modelMapping
+ } else {
+ delete newCredentials.model_mapping
+ }
+
+ updatePayload.credentials = newCredentials
+ }
+
// Antigravity: persist model mapping to credentials (applies to all antigravity types)
// Antigravity 只支持映射模式
if (props.account.platform === 'antigravity') {
diff --git a/frontend/src/components/account/OAuthAuthorizationFlow.vue b/frontend/src/components/account/OAuthAuthorizationFlow.vue
index 08c67494..15c18f51 100644
--- a/frontend/src/components/account/OAuthAuthorizationFlow.vue
+++ b/frontend/src/components/account/OAuthAuthorizationFlow.vue
@@ -603,6 +603,7 @@ const getOAuthKey = (key: string) => {
if (props.platform === 'openai') return `admin.accounts.oauth.openai.${key}`
if (props.platform === 'gemini') return `admin.accounts.oauth.gemini.${key}`
if (props.platform === 'antigravity') return `admin.accounts.oauth.antigravity.${key}`
+ if (props.platform === 'kiro') return `admin.accounts.oauth.kiro.${key}`
return `admin.accounts.oauth.${key}`
}
@@ -632,6 +633,8 @@ const refreshTokenInput = ref('')
const sessionTokenInput = ref('')
const showHelpDialog = ref(false)
const oauthState = ref('')
+const oauthCallbackPath = ref('')
+const oauthLoginOption = ref('')
const projectId = ref('')
// Computed: show method selection when either cookie or refresh token option is enabled
@@ -661,10 +664,10 @@ watch(inputMethod, (newVal) => {
emit('update:inputMethod', newVal)
})
-// Auto-extract code from callback URL (OpenAI/Gemini/Antigravity)
+// Auto-extract code from callback URL (OpenAI/Gemini/Antigravity/Kiro)
// e.g., http://localhost:8085/callback?code=xxx...&state=...
watch(authCodeInput, (newVal) => {
- if (props.platform !== 'openai' && props.platform !== 'gemini' && props.platform !== 'antigravity') return
+ if (props.platform !== 'openai' && props.platform !== 'gemini' && props.platform !== 'antigravity' && props.platform !== 'kiro') return
const trimmed = newVal.trim()
// Check if it looks like a URL with code parameter
@@ -674,7 +677,11 @@ watch(authCodeInput, (newVal) => {
const url = new URL(trimmed)
const code = url.searchParams.get('code')
const stateParam = url.searchParams.get('state')
- if ((props.platform === 'openai' || props.platform === 'gemini' || props.platform === 'antigravity') && stateParam) {
+ if (props.platform === 'kiro') {
+ oauthCallbackPath.value = url.pathname || ''
+ oauthLoginOption.value = url.searchParams.get('login_option') || ''
+ }
+ if ((props.platform === 'openai' || props.platform === 'gemini' || props.platform === 'antigravity' || props.platform === 'kiro') && stateParam) {
oauthState.value = stateParam
}
if (code && code !== trimmed) {
@@ -685,7 +692,13 @@ watch(authCodeInput, (newVal) => {
// If URL parsing fails, try regex extraction
const match = trimmed.match(/[?&]code=([^&]+)/)
const stateMatch = trimmed.match(/[?&]state=([^&]+)/)
- if ((props.platform === 'openai' || props.platform === 'gemini' || props.platform === 'antigravity') && stateMatch && stateMatch[1]) {
+ if (props.platform === 'kiro') {
+ const pathMatch = trimmed.match(/^https?:\/\/[^/]+(\/[^?]*)/)
+ oauthCallbackPath.value = pathMatch?.[1] || oauthCallbackPath.value
+ const loginOptionMatch = trimmed.match(/[?&]login_option=([^&]+)/)
+ oauthLoginOption.value = loginOptionMatch?.[1] || oauthLoginOption.value
+ }
+ if ((props.platform === 'openai' || props.platform === 'gemini' || props.platform === 'antigravity' || props.platform === 'kiro') && stateMatch && stateMatch[1]) {
oauthState.value = stateMatch[1]
}
if (match && match[1] && match[1] !== trimmed) {
@@ -731,6 +744,8 @@ const handleValidateRefreshToken = () => {
defineExpose({
authCode: authCodeInput,
oauthState,
+ oauthCallbackPath,
+ oauthLoginOption,
projectId,
sessionKey: sessionKeyInput,
refreshToken: refreshTokenInput,
@@ -739,6 +754,8 @@ defineExpose({
reset: () => {
authCodeInput.value = ''
oauthState.value = ''
+ oauthCallbackPath.value = ''
+ oauthLoginOption.value = ''
projectId.value = ''
sessionKeyInput.value = ''
refreshTokenInput.value = ''
diff --git a/frontend/src/components/admin/account/AccountTableFilters.vue b/frontend/src/components/admin/account/AccountTableFilters.vue
index b33dad84..358671ff 100644
--- a/frontend/src/components/admin/account/AccountTableFilters.vue
+++ b/frontend/src/components/admin/account/AccountTableFilters.vue
@@ -25,7 +25,7 @@ const updateType = (value: string | number | boolean | null) => { emit('update:f
const updateStatus = (value: string | number | boolean | null) => { emit('update:filters', { ...props.filters, status: value }) }
const updatePrivacyMode = (value: string | number | boolean | null) => { emit('update:filters', { ...props.filters, privacy_mode: value }) }
const updateGroup = (value: string | number | boolean | null) => { emit('update:filters', { ...props.filters, group: value }) }
-const pOpts = computed(() => [{ value: '', label: t('admin.accounts.allPlatforms') }, { value: 'anthropic', label: 'Anthropic' }, { value: 'openai', label: 'OpenAI' }, { value: 'gemini', label: 'Gemini' }, { value: 'antigravity', label: 'Antigravity' }])
+const pOpts = computed(() => [{ value: '', label: t('admin.accounts.allPlatforms') }, { value: 'anthropic', label: 'Anthropic' }, { value: 'openai', label: 'OpenAI' }, { value: 'gemini', label: 'Gemini' }, { value: 'antigravity', label: 'Antigravity' }, { value: 'kiro', label: 'Kiro' }])
const tOpts = computed(() => [{ value: '', label: t('admin.accounts.allTypes') }, { value: 'oauth', label: t('admin.accounts.oauthType') }, { value: 'setup-token', label: t('admin.accounts.setupToken') }, { value: 'apikey', label: t('admin.accounts.apiKey') }, { value: 'bedrock', label: 'AWS Bedrock' }])
const sOpts = computed(() => [{ value: '', label: t('admin.accounts.allStatus') }, { value: 'active', label: t('admin.accounts.status.active') }, { value: 'inactive', label: t('admin.accounts.status.inactive') }, { value: 'error', label: t('admin.accounts.status.error') }, { value: 'rate_limited', label: t('admin.accounts.status.rateLimited') }, { value: 'temp_unschedulable', label: t('admin.accounts.status.tempUnschedulable') }, { value: 'unschedulable', label: t('admin.accounts.status.unschedulable') }])
const privacyOpts = computed(() => [
diff --git a/frontend/src/components/admin/account/ReAuthAccountModal.vue b/frontend/src/components/admin/account/ReAuthAccountModal.vue
index 637d6011..c5d94de1 100644
--- a/frontend/src/components/admin/account/ReAuthAccountModal.vue
+++ b/frontend/src/components/admin/account/ReAuthAccountModal.vue
@@ -2,14 +2,11 @@
-
-
+
- {{
- account.name
- }}
+ {{ account.name }}
{{
isOpenAI
? t('admin.accounts.openaiAccount')
: isGemini
? t('admin.accounts.geminiAccount')
- : isAntigravity
- ? t('admin.accounts.antigravityAccount')
- : t('admin.accounts.claudeCodeAccount')
+ : isKiro
+ ? t('admin.accounts.kiroAccount')
+ : isAntigravity
+ ? t('admin.accounts.antigravityAccount')
+ : t('admin.accounts.claudeCodeAccount')
}}
-
-
{{ t('admin.accounts.oauth.gemini.oauthTypeLabel') }}
@@ -116,7 +109,187 @@
+
+
+ {{ t('admin.accounts.oauth.kiro.authModeTitle') }}
+
+
+
+
+
+
+
+
+
+ {{ t('admin.accounts.oauth.kiro.oauthSubtitle') }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
{{ t('admin.accounts.oauth.kiro.tokenJsonHint') }}
+
+
+
+
+
+
+
+
-
@@ -142,7 +314,16 @@
{{ t('common.cancel') }}
+
@@ -180,28 +357,29 @@