Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ Add the plugin to your `opencode.json` or `opencode.jsonc`:
"variants": {
"low": { "thinkingConfig": { "thinkingBudget": 8192 } },
"medium": { "thinkingConfig": { "thinkingBudget": 16384 } },
"max": { "thinkingConfig": { "thinkingBudget": 32768 } }
"high": { "thinkingConfig": { "thinkingBudget": 32768 } }
}
},
"claude-haiku-4-5": {
Expand All @@ -58,8 +58,18 @@ Add the plugin to your `opencode.json` or `opencode.jsonc`:
"variants": {
"low": { "thinkingConfig": { "thinkingBudget": 8192 } },
"medium": { "thinkingConfig": { "thinkingBudget": 16384 } },
"max": { "thinkingConfig": { "thinkingBudget": 32768 } }
"high": { "thinkingConfig": { "thinkingBudget": 32768 } }
}
},
"claude-sonnet-4-5-1m": {
"name": "Claude Sonnet 4.5 1M",
"limit": { "context": 1000000, "output": 64000 },
"modalities": { "input": ["text", "image", "pdf"], "output": ["text"] }
},
"qwen3-coder-480b": {
"name": "Qwen3 Coder 480B",
"limit": { "context": 200000, "output": 64000 },
"modalities": { "input": ["text"], "output": ["text"] }
}
}
}
Expand Down Expand Up @@ -108,7 +118,9 @@ The plugin supports extensive configuration options. Edit `~/.config/opencode/ki
"usage_sync_max_retries": 3,
"auth_server_port_start": 19847,
"auth_server_port_range": 10,
"builder_id_start_url": "https://view.awsapps.com/start",
"usage_tracking_enabled": true,
"usage_toast_enabled": false,
"enable_log_api_request": false
}
```
Expand All @@ -117,7 +129,7 @@ The plugin supports extensive configuration options. Edit `~/.config/opencode/ki

- `auto_sync_kiro_cli`: Automatically sync sessions from Kiro CLI (default: `true`).
- `account_selection_strategy`: Account rotation strategy (`sticky`, `round-robin`, `lowest-usage`).
- `default_region`: AWS region (`us-east-1`, `us-west-2`).
- `default_region`: AWS region (e.g. `us-east-1`, `eu-west-1`).
- `rate_limit_retry_delay_ms`: Delay between rate limit retries (1000-60000ms).
- `rate_limit_max_retries`: Maximum retry attempts for rate limits (0-10).
- `max_request_iterations`: Maximum loop iterations to prevent hangs (10-1000).
Expand All @@ -126,7 +138,16 @@ The plugin supports extensive configuration options. Edit `~/.config/opencode/ki
- `usage_sync_max_retries`: Retry attempts for usage sync (0-5).
- `auth_server_port_start`: Starting port for auth server (1024-65535).
- `auth_server_port_range`: Number of ports to try (1-100).
- `builder_id_start_url`: Default AWS start URL shown in the auth window (you can override it in the browser UI).

## Authentication UI

The browser page is a single window:

- Enter `Start URL` and `Region`, then click `Begin`.
- The AWS verification page opens only when you click `Open Browser`.
- `usage_tracking_enabled`: Enable usage tracking and toast notifications.
- `usage_toast_enabled`: Show per-account usage toasts (default: `false`).
- `enable_log_api_request`: Enable detailed API request logging.

## Storage
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
{
"name": "@zhafron/opencode-kiro-auth",
"version": "1.5.1",
"version": "1.5.2",
"description": "OpenCode plugin for AWS Kiro (CodeWhisperer) providing access to Claude models",
"type": "module",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc -p tsconfig.build.json",
"test": "npm run build && node --test",
"format": "prettier --write 'src/**/*.ts'",
"typecheck": "tsc --noEmit",
"prepare": "husky"
Expand Down Expand Up @@ -49,6 +50,7 @@
"type": "plugin",
"hooks": [
"auth",
"config",
"event"
]
},
Expand Down
19 changes: 13 additions & 6 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import type { KiroRegion } from './plugin/types'

const VALID_REGIONS: readonly KiroRegion[] = ['us-east-1', 'us-west-2']
const REGION_REGEX = /^[a-z]{2}-[a-z-]+-\d+$/

export function isValidRegion(region: string): region is KiroRegion {
return VALID_REGIONS.includes(region as KiroRegion)
return REGION_REGEX.test(region)
}

export function normalizeRegion(region: string | undefined): KiroRegion {
Expand Down Expand Up @@ -38,14 +38,21 @@ export const KIRO_CONSTANTS = {
}

export const MODEL_MAPPING: Record<string, string> = {
'claude-haiku-4-5': 'claude-haiku-4.5',
'claude-haiku-4-5': 'CLAUDE_HAIKU_4_5_20251001_V1_0',
'claude-haiku-4-5-thinking': 'CLAUDE_HAIKU_4_5_20251001_V1_0',
'claude-sonnet-4-5': 'CLAUDE_SONNET_4_5_20250929_V1_0',
'claude-sonnet-4-5-thinking': 'CLAUDE_SONNET_4_5_20250929_V1_0',
'claude-sonnet-4-5-20250929': 'CLAUDE_SONNET_4_5_20250929_V1_0',
'claude-sonnet-4-5-1m': 'CLAUDE_SONNET_4_5_20250929_LONG_V1_0',
'claude-sonnet-4-5-1m-thinking': 'CLAUDE_SONNET_4_5_20250929_LONG_V1_0',
'claude-opus-4-5': 'CLAUDE_OPUS_4_5_20251101_V1_0',
'claude-opus-4-5-thinking': 'CLAUDE_OPUS_4_5_20251101_V1_0',
'claude-sonnet-4-20250514': 'CLAUDE_SONNET_4_20250514_V1_0',
'claude-3-7-sonnet-20250219': 'CLAUDE_3_7_SONNET_20250219_V1_0'
'claude-sonnet-4': 'CLAUDE_SONNET_4_20250514_V1_0',
'claude-3-7-sonnet': 'CLAUDE_3_7_SONNET_20250219_V1_0',
'nova-swe': 'AGI_NOVA_SWE_V1_5',
'gpt-oss-120b': 'OPENAI_GPT_OSS_120B_1_0',
'qwen3-coder-480b': 'QWEN3_CODER_480B_A35B_1_0',
'minimax-m2': 'MINIMAX_MINIMAX_M2',
'kimi-k2-thinking': 'MOONSHOT_KIMI_K2_THINKING'
}

export const SUPPORTED_MODELS = Object.keys(MODEL_MAPPING)
Expand Down
4 changes: 4 additions & 0 deletions src/core/account/account-selector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ type ToastFunction = (message: string, variant: 'info' | 'warning' | 'success' |
interface AccountSelectorConfig {
auto_sync_kiro_cli: boolean
account_selection_strategy: 'sticky' | 'round-robin' | 'lowest-usage'
usage_tracking_enabled: boolean
usage_toast_enabled: boolean
}

export class AccountSelector {
Expand Down Expand Up @@ -61,6 +63,8 @@ export class AccountSelector {
}

if (
this.config.usage_tracking_enabled &&
this.config.usage_toast_enabled &&
this.accountManager.shouldShowUsageToast() &&
acc.usedCount !== undefined &&
acc.limitCount !== undefined
Expand Down
2 changes: 1 addition & 1 deletion src/core/auth/auth-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export class AuthHandler {
return []
}

const idcMethod = new IdcAuthMethod(this.config, this.repository)
const idcMethod = new IdcAuthMethod(this.config, this.repository, this.accountManager)

return [
{
Expand Down
55 changes: 33 additions & 22 deletions src/core/auth/idc-auth-method.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { exec } from 'node:child_process'
import type { AccountRepository } from '../../infrastructure/database/account-repository.js'
import type { KiroIDCTokenResult } from '../../kiro/oauth-idc.js'
import { authorizeKiroIDC } from '../../kiro/oauth-idc.js'
import type { AccountManager } from '../../plugin/accounts.js'
import { createDeterministicAccountId } from '../../plugin/accounts.js'
import { promptAddAnotherAccount, promptDeleteAccount, promptLoginMode } from '../../plugin/cli.js'
import * as logger from '../../plugin/logger.js'
Expand All @@ -26,7 +26,8 @@ const openBrowser = (url: string) => {
export class IdcAuthMethod {
constructor(
private config: any,
private repository: AccountRepository
private repository: AccountRepository,
private accountManager: AccountManager
) {}

async authorize(inputs?: any): Promise<{
Expand Down Expand Up @@ -90,9 +91,8 @@ export class IdcAuthMethod {
}
while (true) {
try {
const authData = await authorizeKiroIDC(region)
const { url, waitForAuth } = await startIDCAuthServer(
authData,
{ defaultRegion: region, defaultStartUrl: this.config.builder_id_start_url },
this.config.auth_server_port_start,
this.config.auth_server_port_range
)
Expand All @@ -108,9 +108,9 @@ export class IdcAuthMethod {
clientSecret: res.clientSecret
})
if (!u.email) {
console.log('\n[Error] Failed to fetch account email. Skipping...\n')
continue
console.log('\n[Warn] Failed to fetch account email; saving with fallback email.\n')
}
const email = u.email || res.email || 'builder-id@aws.amazon.com'
accounts.push(res as KiroIDCTokenResult)
if (accounts.length === 1 && startFresh) {
const allAccounts = await this.repository.findAll()
Expand All @@ -119,10 +119,10 @@ export class IdcAuthMethod {
await this.repository.delete(acc.id)
}
}
const id = createDeterministicAccountId(u.email, 'idc', res.clientId)
const id = createDeterministicAccountId(email, 'idc', res.clientId)
const acc: ManagedAccount = {
id,
email: u.email,
email,
authMethod: 'idc',
region,
clientId: res.clientId,
Expand All @@ -137,6 +137,7 @@ export class IdcAuthMethod {
limitCount: u.limitCount
}
await this.repository.save(acc)
this.accountManager.addAccount(acc)
const currentCount = (await this.repository.findAll()).length
console.log(`\n[Success] Added: ${u.email} (Quota: ${u.usedCount}/${u.limitCount})\n`)
if (!(await promptAddAnotherAccount(currentCount))) break
Expand All @@ -159,9 +160,8 @@ export class IdcAuthMethod {

private async handleSingleLogin(region: KiroRegion, resolve: any): Promise<void> {
try {
const authData = await authorizeKiroIDC(region)
const { url, waitForAuth } = await startIDCAuthServer(
authData,
{ defaultRegion: region, defaultStartUrl: this.config.builder_id_start_url },
this.config.auth_server_port_start,
this.config.auth_server_port_range
)
Expand All @@ -173,20 +173,30 @@ export class IdcAuthMethod {
callback: async () => {
try {
const res = await waitForAuth()
const u = await fetchUsageLimits({
refresh: '',
access: res.accessToken,
expires: res.expiresAt,
authMethod: 'idc',
region,
clientId: res.clientId,
clientSecret: res.clientSecret
})
if (!u.email) throw new Error('No email')
const id = createDeterministicAccountId(u.email, 'idc', res.clientId)

let u: any = {}
try {
u = await fetchUsageLimits({
refresh: '',
access: res.accessToken,
expires: res.expiresAt,
authMethod: 'idc',
region,
clientId: res.clientId,
clientSecret: res.clientSecret
})
} catch (e) {
logger.warn(
'Failed to fetch usage/email after auth; saving account with fallback email',
e
)
}

const email = u.email || res.email || 'builder-id@aws.amazon.com'
const id = createDeterministicAccountId(email, 'idc', res.clientId)
const acc: ManagedAccount = {
id,
email: u.email,
email,
authMethod: 'idc',
region,
clientId: res.clientId,
Expand All @@ -201,6 +211,7 @@ export class IdcAuthMethod {
limitCount: u.limitCount
}
await this.repository.save(acc)
this.accountManager.addAccount(acc)
return { type: 'success', key: res.accessToken }
} catch (e: any) {
return { type: 'failed' }
Expand Down
26 changes: 19 additions & 7 deletions src/core/request/error-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,13 +101,25 @@ export class ErrorHandler {
let isPermanent = false
try {
const errorBody = await response.text()
const errorData = JSON.parse(errorBody)
if (errorData.reason === 'INVALID_MODEL_ID') {
throw new Error(`Invalid model: ${errorData.message}`)
}
if (errorData.reason === 'TEMPORARILY_SUSPENDED') {
errorReason = 'Account Suspended'
isPermanent = true
try {
const errorData = JSON.parse(errorBody)
if (errorData.reason === 'INVALID_MODEL_ID') {
throw new Error(`Invalid model: ${errorData.message}`)
}
if (errorData.reason === 'TEMPORARILY_SUSPENDED') {
errorReason = 'Account Suspended'
isPermanent = true
} else if (errorData.reason || errorData.message) {
const detail = errorData.reason
? `${errorData.reason}${errorData.message ? `: ${errorData.message}` : ''}`
: errorData.message
errorReason = `${errorReason} (${detail})`
}
} catch (parseError) {
if (errorBody) {
const trimmed = errorBody.replace(/\s+/g, ' ').trim().slice(0, 160)
if (trimmed) errorReason = `${errorReason} (${trimmed})`
}
}
} catch (e) {
if (e instanceof Error && e.message.includes('Invalid model')) {
Expand Down
Loading