diff --git a/README.md b/README.md index 9db840c..2e74895 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,25 @@ [![npm downloads](https://img.shields.io/npm/dm/@zhafron/opencode-kiro-auth)](https://www.npmjs.com/package/@zhafron/opencode-kiro-auth) [![license](https://img.shields.io/npm/l/@zhafron/opencode-kiro-auth)](https://www.npmjs.com/package/@zhafron/opencode-kiro-auth) -OpenCode plugin for AWS Kiro (CodeWhisperer) providing access to Claude Sonnet and Haiku models with substantial trial quotas. +OpenCode plugin for AWS Kiro (CodeWhisperer) providing access to Claude Sonnet and Haiku +models with substantial trial quotas. ## Features -- **Multiple Auth Methods**: Supports AWS Builder ID (IDC), IAM Identity Center (custom Start URL), and Kiro Desktop (CLI-based) authentication. -- **Auto-Sync Kiro CLI**: Automatically imports and synchronizes active sessions from your local `kiro-cli` SQLite database. -- **Gradual Context Truncation**: Intelligently prevents error 400 by reducing context size dynamically during retries. -- **Intelligent Account Rotation**: Prioritizes multi-account usage based on lowest available quota. -- **High-Performance Storage**: Efficient account and usage management using native Bun SQLite. -- **Native Thinking Mode**: Full support for Claude reasoning capabilities via virtual model mappings. -- **Automated Recovery**: Exponential backoff for rate limits and automated token refresh. +- **Multiple Auth Methods**: Supports AWS Builder ID (IDC), IAM Identity Center (custom + Start URL), and Kiro Desktop (CLI-based) authentication. +- **Auto-Sync Kiro CLI**: Automatically imports and synchronizes active sessions from + your local `kiro-cli` SQLite database. +- **Gradual Context Truncation**: Intelligently prevents error 400 by reducing context + size dynamically during retries. +- **Intelligent Account Rotation**: Prioritizes multi-account usage based on lowest + available quota. +- **High-Performance Storage**: Efficient account and usage management using native Bun + SQLite. +- **Native Thinking Mode**: Full support for Claude reasoning capabilities via virtual + model mappings. +- **Automated Recovery**: Exponential backoff for rate limits and automated token + refresh. ## Installation @@ -126,11 +134,12 @@ Add the plugin to your `opencode.json` or `opencode.jsonc`: "max": { "thinkingConfig": { "thinkingBudget": 32768 } } } }, - "qwen3-coder-480b": { - "name": "Qwen3 Coder 480B", - "limit": { "context": 200000, "output": 64000 }, - "modalities": { "input": ["text"], "output": ["text"] } - } + "auto": { "name": "Auto (1.0x)" }, + "claude-sonnet-4": { "name": "Claude Sonnet 4.0 (1.3x)", "limit": { "context": 200000, "output": 64000 } }, + "deepseek-3.2": { "name": "DeepSeek 3.2 (0.25x)", "limit": { "context": 128000, "output": 64000 } }, + "minimax-m2.5": { "name": "MiniMax 2.5 (0.25x)", "limit": { "context": 200000, "output": 64000 } }, + "minimax-m2.1": { "name": "MiniMax 2.1 (0.15x)", "limit": { "context": 200000, "output": 64000 } }, + "qwen3-coder-next": { "name": "Qwen3 Coder Next (0.05x)", "limit": { "context": 256000, "output": 64000 } } } } } @@ -142,26 +151,37 @@ Add the plugin to your `opencode.json` or `opencode.jsonc`: 1. **Authentication via Kiro CLI (Recommended)**: - Perform login directly in your terminal using `kiro-cli login`. - The plugin will automatically detect and import your session on startup. - - For AWS IAM Identity Center (SSO/IDC), the plugin imports both the token and device registration (OIDC client credentials) from the `kiro-cli` database. + - For AWS IAM Identity Center (SSO/IDC), the plugin imports both the token and device + registration (OIDC client credentials) from the `kiro-cli` database. 2. **Direct Authentication**: - Run `opencode auth login`. - Select `Other`, type `kiro`, and press enter. - - You'll be prompted for your **IAM Identity Center Start URL** and **IAM Identity Center region** (`sso_region`). + - You'll be prompted for your **IAM Identity Center Start URL** and **IAM Identity + Center region** (`sso_region`). - Leave it blank to sign in with **AWS Builder ID**. - - Enter your company's Start URL (e.g. `https://your-company.awsapps.com/start`) to use **IAM Identity Center (SSO)**. - - Note: the TUI `/connect` flow currently does **not** run plugin OAuth prompts (Start URL / region), so Identity Center logins may fall back to Builder ID unless you use `opencode auth login` (or preconfigure defaults in `~/.config/opencode/kiro.json`). + - Enter your company's Start URL (e.g. `https://your-company.awsapps.com/start`) to + use **IAM Identity Center (SSO)**. + - Note: the TUI `/connect` flow currently does **not** run plugin OAuth prompts + (Start URL / region), so Identity Center logins may fall back to Builder ID unless + you use `opencode auth login` (or preconfigure defaults in + `~/.config/opencode/kiro.json`). - For **IAM Identity Center**, you may also need a **profile ARN** (`profileArn`). - - If `kiro-cli` is installed and you've selected a profile once (`kiro-cli profile`), the plugin auto-detects it. + - If `kiro-cli` is installed and you've selected a profile once + (`kiro-cli profile`), the plugin auto-detects it. - Otherwise, set `idc_profile_arn` in `~/.config/opencode/kiro.json`. - - A browser window will open directly to AWS' verification URL (no local auth server). If it doesn't, copy/paste the URL and enter the code printed by OpenCode. - - You can also pre-configure defaults in `~/.config/opencode/kiro.json` via `idc_start_url` and `idc_region`. + - A browser window will open directly to AWS' verification URL (no local auth + server). If it doesn't, copy/paste the URL and enter the code printed by OpenCode. + - You can also pre-configure defaults in `~/.config/opencode/kiro.json` via + `idc_start_url` and `idc_region`. 3. Configuration will be automatically managed at `~/.config/opencode/kiro.db`. ## Local plugin development -OpenCode installs plugins into a cache directory (typically `~/.cache/opencode/node_modules`). +OpenCode installs plugins into a cache directory (typically +`~/.cache/opencode/node_modules`). -The simplest way to test local changes (without publishing to npm) is to build this repo and hot-swap the cached plugin `dist/` folder: +The simplest way to test local changes (without publishing to npm) is to build this repo +and hot-swap the cached plugin `dist/` folder: 1. Build this repo: `bun run build` (or `npm run build`) 2. Hot-swap `dist/` (creates a timestamped backup): @@ -187,9 +207,11 @@ mv "$PLUGIN_DIR/dist.bak.YYYYMMDD-HHMMSS" "$PLUGIN_DIR/dist" ### Error: Status: 403 (AccessDeniedException / User is not authorized) -If you're using **IAM Identity Center** (a custom Start URL), the Q Developer / CodeWhisperer APIs typically require a **profile ARN**. +If you're using **IAM Identity Center** (a custom Start URL), the Q Developer / +CodeWhisperer APIs typically require a **profile ARN**. -This plugin reads the active profile ARN from your local `kiro-cli` database (`state.key = api.codewhisperer.profile`) and sends it as `profileArn`. +This plugin reads the active profile ARN from your local `kiro-cli` database +(`state.key = api.codewhisperer.profile`) and sends it as `profileArn`. Fix: @@ -202,17 +224,25 @@ This happens when the plugin has no records in `~/.config/opencode/kiro.db`. 1. Ensure `kiro-cli login` succeeds. 2. Ensure `auto_sync_kiro_cli` is `true` in `~/.config/opencode/kiro.json`. -3. Retry the request; the plugin will attempt a Kiro CLI sync when it detects zero accounts. +3. Retry the request; the plugin will attempt a Kiro CLI sync when it detects zero + accounts. ### Note: `/connect` vs `opencode auth login` -If you need to enter provider-specific values for an OAuth login (like IAM Identity Center Start URL / region), use `opencode auth login`. The current TUI `/connect` flow may not display plugin OAuth prompts, so it can’t collect those inputs. +If you need to enter provider-specific values for an OAuth login (like IAM Identity +Center Start URL / region), use `opencode auth login`. The current TUI `/connect` flow +may not display plugin OAuth prompts, so it can’t collect those inputs. -Note for IDC/SSO (ODIC): the plugin may temporarily create an account with a placeholder email if it cannot fetch the real email during sync (e.g. offline). It will replace it with the real email once usage/email lookup succeeds. +Note for IDC/SSO (ODIC): the plugin may temporarily create an account with a placeholder +email if it cannot fetch the real email during sync (e.g. offline). +It will replace it with the real email once usage/email lookup succeeds. ### Kiro CLI (Google/GitHub OAuth) users: plugin sync never runs -If you authenticated via `kiro-cli login` using Google or GitHub OAuth (not AWS Builder ID or IAM Identity Center), the plugin's sync may never trigger. This happens because OpenCode requires a kiro entry in `auth.json` before making API requests, but the plugin loader only runs when a request is made. +If you authenticated via `kiro-cli login` using Google or GitHub OAuth (not AWS Builder +ID or IAM Identity Center), the plugin's sync may never trigger. +This happens because OpenCode requires a kiro entry in `auth.json` before making API +requests, but the plugin loader only runs when a request is made. **Workaround:** Add a minimal placeholder entry to `~/.local/share/opencode/auth.json`: @@ -225,15 +255,19 @@ If you authenticated via `kiro-cli login` using Google or GitHub OAuth (not AWS } ``` -After adding this, OpenCode will treat the provider as connected, trigger the plugin loader, and the kiro-cli sync will populate `kiro.db` with your actual tokens. The placeholder values are not used for API calls. +After adding this, OpenCode will treat the provider as connected, trigger the plugin +loader, and the kiro-cli sync will populate `kiro.db` with your actual tokens. +The placeholder values are not used for API calls. -**Important:** Ensure `auto_sync_kiro_cli` is `true` in `~/.config/opencode/kiro.json` and that `kiro-cli login` succeeds before applying this workaround. +**Important:** Ensure `auto_sync_kiro_cli` is `true` in `~/.config/opencode/kiro.json` +and that `kiro-cli login` succeeds before applying this workaround. ### Error: ERR_INVALID_URL `TypeError [ERR_INVALID_URL]: "undefined/chat/completions" cannot be parsed as a URL` -If this happens, check your auth.json in .local/share/opencode. example: +If this happens, check your auth.json in .local/share/opencode. +example: ```json { @@ -246,7 +280,8 @@ If this happens, check your auth.json in .local/share/opencode. example: ## Configuration -The plugin supports extensive configuration options. Edit `~/.config/opencode/kiro.json`: +The plugin supports extensive configuration options. +Edit `~/.config/opencode/kiro.json`: ```json { @@ -269,10 +304,14 @@ The plugin supports extensive configuration options. Edit `~/.config/opencode/ki ### Configuration Options - `auto_sync_kiro_cli`: Automatically sync sessions from Kiro CLI (default: `true`). -- `account_selection_strategy`: Account rotation strategy (`sticky`, `round-robin`, `lowest-usage`). +- `account_selection_strategy`: Account rotation strategy (`sticky`, `round-robin`, + `lowest-usage`). - `default_region`: AWS region (`us-east-1`, `us-west-2`). -- `idc_start_url`: Default IAM Identity Center Start URL (e.g. `https://your-company.awsapps.com/start`). Leave unset/blank to default to AWS Builder ID. -- `idc_region`: IAM Identity Center (SSO OIDC) region (`sso_region`). Defaults to `us-east-1`. +- `idc_start_url`: Default IAM Identity Center Start URL (e.g. + `https://your-company.awsapps.com/start`). Leave unset/blank to default to AWS Builder + ID. +- `idc_region`: IAM Identity Center (SSO OIDC) region (`sso_region`). Defaults to + `us-east-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). @@ -298,10 +337,14 @@ The plugin supports extensive configuration options. Edit `~/.config/opencode/ki ## Acknowledgements -Special thanks to [AIClient-2-API](https://github.com/justlovemaki/AIClient-2-API) for providing the foundational Kiro authentication logic and request patterns. +Special thanks to [AIClient-2-API](https://github.com/justlovemaki/AIClient-2-API) for +providing the foundational Kiro authentication logic and request patterns. ## Disclaimer -This plugin is provided strictly for learning and educational purposes. It is an independent implementation and is not affiliated with, endorsed by, or supported by Amazon Web Services (AWS) or Anthropic. Use of this plugin is at your own risk. +This plugin is provided strictly for learning and educational purposes. +It is an independent implementation and is not affiliated with, endorsed by, or +supported by Amazon Web Services (AWS) or Anthropic. +Use of this plugin is at your own risk. Feel free to open a PR to optimize this plugin further. diff --git a/models.dev-info/models/auto.toml b/models.dev-info/models/auto.toml new file mode 100644 index 0000000..0ec81ca --- /dev/null +++ b/models.dev-info/models/auto.toml @@ -0,0 +1,5 @@ +name = "Auto" + +[cost] +input = 0 +output = 0 diff --git a/models.dev-info/models/claude-sonnet-4-5.toml b/models.dev-info/models/claude-sonnet-4-5.toml new file mode 100644 index 0000000..d36f339 --- /dev/null +++ b/models.dev-info/models/claude-sonnet-4-5.toml @@ -0,0 +1,18 @@ +name = "Claude Sonnet 4.5" +attachment = true +reasoning = true +tool_call = true +temperature = true +open_weights = false + +[cost] +input = 0 +output = 0 + +[limit] +context = 200_000 +output = 64_000 + +[modalities] +input = ["text", "image", "pdf"] +output = ["text"] diff --git a/models.dev-info/models/claude-sonnet-4.toml b/models.dev-info/models/claude-sonnet-4.toml new file mode 100644 index 0000000..228ebdf --- /dev/null +++ b/models.dev-info/models/claude-sonnet-4.toml @@ -0,0 +1,18 @@ +name = "Claude Sonnet 4.0" +attachment = true +reasoning = true +tool_call = true +temperature = true +open_weights = false + +[cost] +input = 0 +output = 0 + +[limit] +context = 200_000 +output = 64_000 + +[modalities] +input = ["text", "image", "pdf"] +output = ["text"] diff --git a/models.dev-info/models/deepseek-3.2.toml b/models.dev-info/models/deepseek-3.2.toml new file mode 100644 index 0000000..19d9161 --- /dev/null +++ b/models.dev-info/models/deepseek-3.2.toml @@ -0,0 +1,18 @@ +name = "DeepSeek 3.2" +attachment = false +reasoning = true +tool_call = true +temperature = true +open_weights = true + +[cost] +input = 0 +output = 0 + +[limit] +context = 128_000 +output = 64_000 + +[modalities] +input = ["text"] +output = ["text"] diff --git a/models.dev-info/models/minimax-m2.1.toml b/models.dev-info/models/minimax-m2.1.toml new file mode 100644 index 0000000..2852225 --- /dev/null +++ b/models.dev-info/models/minimax-m2.1.toml @@ -0,0 +1,18 @@ +name = "MiniMax 2.1" +attachment = false +reasoning = true +tool_call = true +temperature = true +open_weights = false + +[cost] +input = 0 +output = 0 + +[limit] +context = 204_800 +output = 131_072 + +[modalities] +input = ["text"] +output = ["text"] diff --git a/models.dev-info/models/minimax-m2.5.toml b/models.dev-info/models/minimax-m2.5.toml new file mode 100644 index 0000000..bd4a91e --- /dev/null +++ b/models.dev-info/models/minimax-m2.5.toml @@ -0,0 +1,18 @@ +name = "MiniMax 2.5" +attachment = false +reasoning = true +tool_call = true +temperature = true +open_weights = false + +[cost] +input = 0 +output = 0 + +[limit] +context = 204_800 +output = 131_072 + +[modalities] +input = ["text"] +output = ["text"] diff --git a/models.dev-info/models/qwen3-coder-next.toml b/models.dev-info/models/qwen3-coder-next.toml new file mode 100644 index 0000000..50bd426 --- /dev/null +++ b/models.dev-info/models/qwen3-coder-next.toml @@ -0,0 +1,18 @@ +name = "Qwen3 Coder Next" +attachment = false +reasoning = false +tool_call = true +temperature = true +open_weights = true + +[cost] +input = 0 +output = 0 + +[limit] +context = 256_000 +output = 64_000 + +[modalities] +input = ["text"] +output = ["text"] diff --git a/models.dev-info/provider.toml b/models.dev-info/provider.toml new file mode 100644 index 0000000..d1cba5e --- /dev/null +++ b/models.dev-info/provider.toml @@ -0,0 +1,3 @@ +name = "AWS Kiro" +env = [] +doc = "https://kiro.dev" diff --git a/src/__tests__/model-resolution.test.ts b/src/__tests__/model-resolution.test.ts new file mode 100644 index 0000000..9fd1610 --- /dev/null +++ b/src/__tests__/model-resolution.test.ts @@ -0,0 +1,113 @@ +import { Database } from 'bun:sqlite' +import { beforeAll, describe, expect, test } from 'bun:test' +import { existsSync } from 'node:fs' +import { KIRO_CONSTANTS, buildUrl } from '../constants.js' +import { resolveKiroModel } from '../plugin/models.js' + +// Integration tests - prove model slugs are accepted by the AWS API. +// Requires valid auth token in kiro.db (skipped if not available). +describe('API model slug validation', () => { + const KIRO_DB_PATH = process.env.KIRO_DB_PATH || `${process.env.HOME}/.config/opencode/kiro.db` + let accessToken: string | null = null + let region = 'us-east-1' + + beforeAll(() => { + if (!existsSync(KIRO_DB_PATH)) return + + try { + const db = new Database(KIRO_DB_PATH, { readonly: true }) + const row = db + .prepare( + `SELECT access_token, region FROM accounts + WHERE is_healthy = 1 + ORDER BY expires_at DESC LIMIT 1` + ) + .get() as any + if (row?.access_token) { + accessToken = row.access_token + region = row.region || 'us-east-1' + } + db.close() + } catch { + // DB read failed, tests will be skipped + } + }) + + async function apiRequest( + modelId: string + ): Promise<{ invalidModel: boolean; status: number; body: string }> { + const url = buildUrl(KIRO_CONSTANTS.BASE_URL, region as any) + const body = JSON.stringify({ + conversationState: { + chatTriggerType: 'MANUAL', + conversationId: `test-${Date.now()}`, + currentMessage: { + userInputMessage: { + content: 'test', + modelId, + origin: 'AI_EDITOR' + } + } + } + }) + + const res = await fetch(url, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + Authorization: `Bearer ${accessToken}`, + 'x-amzn-kiro-agent-mode': 'vibe', + 'amz-sdk-invocation-id': crypto.randomUUID(), + 'amz-sdk-request': 'attempt=1; max=1' + }, + body + }) + + const text = await res.text().catch(() => '') + return { + invalidModel: text.includes('INVALID_MODEL_ID'), + status: res.status, + body: text.slice(0, 200) + } + } + + test('invalid model slug is rejected by API with INVALID_MODEL_ID', async () => { + if (!accessToken) return + + const result = await apiRequest('this-model-does-not-exist') + expect(result.invalidModel).toBe(true) + expect(result.body).toContain('INVALID_MODEL_ID') + }) + + test('auto is accepted by API', async () => { + if (!accessToken) return + + const result = await apiRequest(resolveKiroModel('auto')) + expect(result.invalidModel).toBe(false) + }) + + test( + 'deepseek-3.2 is accepted by API', + async () => { + if (!accessToken) return + + const result = await apiRequest(resolveKiroModel('deepseek-3.2')) + expect(result.invalidModel).toBe(false) + }, + { timeout: 10000 } + ) + + test('claude-sonnet-4-5 is accepted by API', async () => { + if (!accessToken) return + + const result = await apiRequest(resolveKiroModel('claude-sonnet-4-5')) + expect(result.invalidModel).toBe(false) + }) + + test('qwen3-coder-480b (deleted) is rejected by API', async () => { + if (!accessToken) return + + const result = await apiRequest('QWEN3_CODER_480B_A35B_1_0') + expect(result.invalidModel).toBe(true) + }) +}) diff --git a/src/constants.ts b/src/constants.ts index 6323805..333a79a 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -72,7 +72,12 @@ export const MODEL_MAPPING: Record = { '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' + 'kimi-k2-thinking': 'MOONSHOT_KIMI_K2_THINKING', + auto: 'auto', + 'deepseek-3.2': 'deepseek-3.2', + 'minimax-m2.5': 'minimax-m2.5', + 'minimax-m2.1': 'minimax-m2.1', + 'qwen3-coder-next': 'qwen3-coder-next' } export const SUPPORTED_MODELS = Object.keys(MODEL_MAPPING)