kiro-proxy is a headless Rust service that exposes Claude Messages and OpenAI
Chat Completions compatible APIs on top of Kiro upstream services. It includes
multi-account scheduling, automatic token refresh, endpoint failover, model
mapping, API-key quotas, TLS, webhooks, statistics, and an operations CLI.
Important
Account support is limited to Kiro enterprise accounts authenticated through the enterprise's SSO integration (AWS IAM Identity Center/IdC). All other account and authentication types, including personal and social-login accounts, are not supported.
This repository intentionally does not include a GUI, MITM support, or local Kiro application configuration changes.
- Claude-compatible
/v1/messagesand/v1/messages/count_tokensendpoints. - OpenAI-compatible
/v1/chat/completionsand/v1/modelsendpoints. - Weighted multi-account scheduling with per-account concurrency limits, cooldowns, quota tracking, and model compatibility checks.
- Automatic enterprise IdC/SSO token refresh with per-account singleflight.
- Account-aware Amazon Q and CodeWhisperer endpoint selection with bounded, in-memory availability caches.
- Dynamic model discovery, model aliases, replacements, load balancing, and fallback rules.
- Unix-socket administration through the
kproxyCLI; no browser UI is required. - Hot-reloaded TOML configuration, structured logs, trace IDs, statistics, API-key limits, TLS, and webhook notifications.
- Automatic
.envloading by bothkproxydandkproxyon every startup.
| Component | Purpose |
|---|---|
kproxyd |
Long-running proxy daemon and administration server. |
kproxy |
Headless administration CLI. |
kproxy-core |
Domain models, defaults, and configuration validation. |
kproxy-store |
Atomic persistence, .env loading, bootstrap, and hot reload. |
kproxy-ipc |
Line-delimited JSON-RPC protocol shared by daemon and CLI. |
kproxy-translate |
Claude/OpenAI/Kiro translation, validation, and token estimation. |
kproxy-kiro |
Kiro HTTP client, Event Stream decoding, endpoint state, and model discovery. |
kproxy-pool |
Account health, credit reservation, concurrency, and weighted scheduling. |
kproxy-notify |
Webhook delivery, retries, suppression, and credit alerts. |
The CLI source is located in crates/kproxy, and the daemon source
is located in crates/kproxyd.
Docker Engine with the Compose v2 plugin is the shortest production setup. The
Compose stack pulls the prebuilt full image with all features and Chromium,
runs kproxyd with host networking, and keeps all state in the kproxy-data
named volume. Run the one-step setup from the repository root. It validates the
environment, pulls the image before replacing the container, waits for health,
rolls back automatically on failure, and installs the kproxy command on the
host:
./deploy/docker-setup.sh
kproxy health
kproxy statusThe default target is /usr/local/bin/kproxy; the script invokes sudo when
needed. Without sudo access, install it in a user-owned directory:
./deploy/docker-setup.sh --target "$HOME/.local/bin/kproxy"The host command is a small wrapper that runs the matching CLI inside the container. This avoids Linux-container binary incompatibility on hosts such as macOS and keeps the administration Unix socket private. Deploy an immutable release tag explicitly; the successful image reference is saved locally for subsequent restarts:
./deploy/docker-setup.sh --image ghcr.io/yaocool/kiro-proxy:v0.1.3To automatically follow the newest stable image published as latest, run:
./deploy/docker-upgrade.shIt checks the registry even when an older image reference was saved by a previous deployment, and retains the same health-check and rollback behavior.
Use --no-pull for an offline restart of the saved image. Use --build only
when a deliberate local source build is required. GHCR packages are private by
default unless their visibility is changed; private deployments must run
docker login ghcr.io with an account/token that can read the package.
The wrapper also manages the Docker service lifecycle from the host:
kproxy restart # Restart and wait until the health check passes
kproxy stop # Stop; restart remains available afterwards
kproxy uninstall # Completely uninstall the service
kproxy uninstall --backup-dir /srv/kproxy-backupsuninstall first stops the daemon gracefully, copies all of /var/lib/kproxy
to the host, and verifies that config.toml exists before removing the
container, persistent data volume, unshared image, and installed wrapper. A
backup failure aborts the uninstall and starts the original container again.
The default backup root is ~/.kproxy/backups; override it with --backup-dir
or KPROXY_BACKUP_DIR. Interactive use asks whether to retain the backup.
--yes retains it by default, and only an explicit --delete-backup removes it
after a successful uninstall. The source checkout is always retained.
On Linux, the script also checks the named volume before changing the container.
If Docker retains the volume metadata but its host data directory is gone, an
interactive run offers to recreate it; automation can opt in with
--repair-volume. Repair is allowed only when the volume belongs to this
Compose project, Docker's volume root is available, and the data path is truly
missing. A missing disk mount or unsafe symlink stops the setup instead of
hiding potentially recoverable data.
A fresh daemon exposes only its Unix administration socket. Create the first business proxy explicitly and save the API key printed by the command:
kproxy status
kproxy service create --name main
kproxy service listImport at least one supported Kiro enterprise SSO account before sending generation requests:
kproxy account import --stdin < accounts.json
kproxy account probe --allThe default listener is 0.0.0.0:5580. Restrict that port with the host
firewall or cloud security group; use --host 127.0.0.1 when the proxy should
only be reachable from the Docker host. Do not run docker compose down -v
unless the persisted configuration, accounts, usage, and logs should be erased.
The pinned Rust 1.97.1 toolchain is selected automatically through
rust-toolchain.toml.
cp .env.example .env
cargo build --release --locked
# First startup creates config.toml, accounts.json, daily.json, and stats.json.
./target/release/kproxydIn another terminal:
./target/release/kproxy health
./target/release/kproxy status
./target/release/kproxy service create --name main
./target/release/kproxy config path
./target/release/kproxy account listkproxy service create creates and starts a proxy, creates its first scoped API
key, and prints the plaintext key. You can retrieve that service's keys later
with kproxy service apikeys main --show-secret.
kproxyd and kproxy search for .env from the current directory upward. Existing
process environment variables take precedence over values in .env, so a
one-off override remains possible:
KPROXY_HTTP_PORT=5581 ./target/release/kproxydAfter creating main with the default settings, its business API binds to
0.0.0.0:5580; use http://127.0.0.1:5580 from the same host:
POST /v1/messages
POST /v1/messages/count_tokens
POST /v1/chat/completions
GET /v1/models
GET /health
Claude aliases /messages and /anthropic/v1/messages are also available.
OpenAI aliases /chat/completions and /models are supported as well.
Claude Code loads every MCP schema up front when ANTHROPIC_BASE_URL points to
a third-party proxy unless Tool Search is explicitly enabled. For large MCP
catalogs, start Claude Code with:
ANTHROPIC_BASE_URL=http://127.0.0.1:5580 ENABLE_TOOL_SEARCH=auto claudekiro-proxy accepts Anthropic defer_loading, regex/BM25 Tool Search, and
tool_reference history blocks. Because Kiro has no native Tool Search server
tool, the proxy executes the search locally and continues the same response.
The official Tool Search input contains pattern or query plus an optional
limit from 1 to 10,000 (default 5). kiro-proxy honors that requested limit
and then packs results against the remaining tool-count, tool-token, context,
and payload-byte budgets; there is no fixed five-tool working set. Deferred
definitions remain outside the Kiro context and payload until discovered. The
catalog index and searches run on blocking workers rather than HTTP runtime
threads.
The generated [context] configuration also bounds the loaded working set with
max_loaded_tools (default and proxy ceiling 512), deferred Tool
Search working-set definitions with max_tool_input_tokens, and the serialized
Kiro request with max_upstream_payload_bytes. Ordinary requests without Tool
Search are not subject to that 32k working-set budget: their definitions remain
part of the model's total input-token estimate and are still bounded by the
context window, tool count, and payload size. Truly oversized requests fail
locally instead of producing an opaque upstream error. HTTP
413/request_too_large is reserved for an actual inbound body over 50 MiB;
tool, context, and translated-payload budget errors use 400 so Claude Code does
not misreport them as a 32 MB attachment failure.
Mapping-aware context compaction for Claude Messages is enabled by default with
context.auto_compact_on_overflow. Before the first upstream generation, the
proxy compacts against the mapped model's safe window. If upstream still
returns prompt is too long or context length exceeded, the proxy compacts
against a conservative window and retries only once. A semantic-summary request
never receives the original oversized conversation directly: the local
tokenizer first creates a bounded checkpoint for the summary model. The same
compaction artifact may also be reapplied once if the selected account resolves
to a smaller window. OpenAI Chat
Completions and context growth after a Tool Search response has started retain
hard context-limit errors because they cannot safely return a leading Claude
compaction boundary. A summary timeout releases the main request immediately;
late accounting is allowed only for a bounded grace period, after which the
summary stream is canceled and any already decoded usage is settled.
features.tool_search_max_rounds defaults to 4 and is hard-clamped to 8. If
that per-request server loop is exhausted, the response uses Claude's
pause_turn continuation state instead of converting a valid server call into
an HTTP 5xx. features.tool_search_max_operations defaults to 32 (valid range
1–256) and bounds aggregate searches across resumed calls and all internal
rounds; excess calls receive an in-band unavailable Tool Search result.
features.enable_tool_search=false is a rollback switch: native Tool Search
requests are then rejected explicitly instead of expanding deferred tools into
the upstream payload. Request logs retain catalog/working-set sizes, search
limits and truncation, client/upstream status, and a stable error code. Error
responses keep the normal Claude/OpenAI body and expose diagnostics through
request-id, x-kproxy-error-code, x-kproxy-error-stage,
x-kproxy-upstream-status, and x-kproxy-account-error headers.
For Claude's native web_search server tool, Kiro first decides whether to
search and chooses the query. The proxy then calls Kiro's /mcp JSON-RPC
web_search, returns the real result as a tool result to the same model turn,
and lets Kiro synthesize the final answer. It does not search the first user
message eagerly or present a raw search summary as model output. Streaming and
non-streaming Claude responses use server_tool_use and
web_search_tool_result; search failures are in-band tool errors and do not
cool down or ban the account. Parallel searches are preserved. In mixed
server/client-tool turns the server call remains pending until the client tool
results are returned, matching Anthropic's continuation protocol. Results carry
proxy-owned AES-256-GCM replay content so later turns can restore snippets;
modified records are rejected before entering model context. Anthropic-owned
opaque values remain accepted but are not decrypted locally. Final text carries
a structured web_search_result_location citation only when it actually
includes the exact result URL. The proxy
safety limit defaults to 20 searches; an explicit max_uses above that
configured limit is rejected instead of being silently clamped. Claude Web
Fetch is rejected explicitly until a compatible server-side executor is
implemented.
The default MCP URL is https://runtime.{region}.kiro.dev/mcp. Override it with
upstream.web_search_endpoint (the {region} placeholder is supported) or the
temporary KPROXY_MCP_URL environment variable. The default
upstream.web_search_timeout_ms is 60,000. Every MCP request carries Kiro's
required x-amzn-kiro-profile-arn header. When an imported account has no
profile ARN, the proxy discovers it through ListAvailableProfiles, collapses
concurrent discovery for the same token, and persists the result. Builder ID
and Social accounts use Kiro's compatible fixed-profile fallback.
Domain/location filters, code-execution callers, strict schemas, and eager
streaming are rejected when Kiro cannot provide equivalent semantics. The
proxy-generated encrypted fields
are explicitly proxy-owned and are not claimed to be interoperable with
Anthropic's hosted-search ciphertext.
Use .env for startup-path selection and temporary process overrides. Use
config.toml for persistent service, pool, model, API-key, TLS, logging, and
notification settings. See .env.example for every supported
example variable and its purpose.
Set KPROXY_HOME to place configuration, data, logs, and the administration socket
under one directory. Without KPROXY_HOME, XDG locations are used:
| File | Default location | Notes |
|---|---|---|
config.toml |
${XDG_CONFIG_HOME:-~/.config}/kproxy/ |
Human-edited daemon configuration. |
accounts.json |
${XDG_DATA_HOME:-~/.local/share}/kproxy/ |
Contains credentials; created with mode 0600. |
daily.json |
${XDG_DATA_HOME:-~/.local/share}/kproxy/ |
Daily credit accounting, reset on UTC boundaries. |
stats.json |
${XDG_DATA_HOME:-~/.local/share}/kproxy/ |
Persisted aggregate request statistics. |
stats-history/ |
${XDG_DATA_HOME:-~/.local/share}/kproxy/ |
One-minute request aggregates split into bounded UTC hourly shards. |
web-search-replay.key |
${XDG_DATA_HOME:-~/.local/share}/kproxy/ |
AES-256-GCM replay key; created with mode 0600 and never overwritten. |
admin.sock |
${XDG_RUNTIME_DIR}/kproxy/ or /run/kproxy/ |
Local administration plane. |
| Logs | ${XDG_DATA_HOME:-~/.local/share}/kproxy/logs/ |
Split by UTC date and severity. |
On first startup, missing files are created without overwriting existing data.
Valid configuration changes are hot-reloaded. Invalid TOML or validation
failures leave the last valid configuration active. server.host and
server.port are defaults for newly created proxy services. Changes to
admin.socket or the shared HTTP/HTTPS listening mode require a daemon restart;
most other fields, including the proxy service list, apply without one.
External account-file changes are also reloaded. Corrupt account data never replaces the valid in-memory snapshot. Large account stores can use a gzip envelope plus incremental sidecar updates according to the storage settings.
Only credentials issued to a Kiro enterprise account through its organization SSO may be imported. Importing a credential does not make personal, social-login, or any other account type compatible. Import supported credentials from a JSON file or stdin:
kproxy account import --file accounts.json
cat accounts.json | kproxy account import --stdinid, machine_id, and created_at may be omitted; the CLI generates them.
[
{
"email": "user@example.com",
"credentials": {
"access_token": "...",
"refresh_token": "...",
"client_id": "...",
"client_secret": "...",
"region": "us-east-1",
"expires_at": 1767225600,
"auth_method": "idc"
}
}
]Account exports contain credentials by default. Use --redact before sharing
diagnostic output:
kproxy --json account export --redactkproxy status
kproxy health
kproxy service list
kproxy service show main
kproxy service create --name main --port 5580
kproxy service edit main --port 5581 --add-api-key ci
kproxy service disable main
kproxy service enable main
kproxy service apikeys main
kproxy service apikeys main --show-secret
kproxy service delete main
kproxy account list
kproxy account show <id|email>
kproxy account tag <id|email> --add prod
kproxy account disable <id|email>
kproxy account refresh <id|email>
kproxy account refresh --all
kproxy account probe --all
kproxy account regen-machine-id <id|email>
kproxy account rm <id|email> [<id|email> ...]
kproxy config list
kproxy config show server
kproxy config show pool --effective
kproxy config edit pool # edits only one module; bare edit still opens the full file
kproxy config reset pool # resets only one module and preserves every other module
kproxy config path
kproxy config validate
kproxy config reload
kproxy config reset # resets all general settings, preserves API keys/services
kproxy pool --watch --explain
kproxy diagnose endpoints
kproxy diagnose account --all -c 4 --timeout 45s
kproxy subscriptions
kproxy models --refresh --mapped
kproxy models resolve opus5 # show model-map and final per-account Kiro model
kproxy model-map add --name low-credit --source 'claude-opus-*' --target claude-sonnet-4.6 --below-credits-percent 10
kproxy model-map edit low-credit --below-credits-percent 15
kproxy model-map delete low-credit
kproxy model-map test claude-opus-4
kproxy apikey list
kproxy apikey list --detail
kproxy apikey show ci
kproxy apikey add --name ci
kproxy apikey add --name recovered --key 'sk-original-key' # restore a deleted key
kproxy apikey limit ci --credits 100
kproxy apikey limit ci --clear
kproxy alert events
kproxy alert platforms
kproxy alert config
kproxy alert add --name alerts --platform dingtalk --webhook-url 'https://oapi.dingtalk.com/robot/send?access_token=replace-me' --dingtalk-sign 'SEC-replace-me' --event token-refresh-failed,account-credit-protected,account-quota-exhausted,service-quota-exhausted
kproxy alert edit --name alerts --event token-refresh-failed --event service-quota-exhausted
kproxy alert delete alerts
kproxy status --since 30m
kproxy stats --since 1h
kproxy stats --start 2026-08-27T10:00:00+08:00 --end 2026-08-27T12:00:00+08:00
kproxy stats --detail --since 1h --by endpoint
kproxy logs show --tail 100
kproxy logs follow --level warn
kproxy logs trace trace_0123456789abcdef0123456789abcdef
kproxy logs files
kproxy logs files --level error
kproxy logs path
kproxy tasks
kproxy tasks run status_check
kproxy helpAll commands support the global --json option. Run kproxy --help or a
subcommand's --help for the authoritative option list.
Every config.toml mutation performed through kproxy or the daemon holds the
same cross-process file lock across the complete read, update, atomic write,
runtime reload, and rollback transaction. Later concurrent mutations wait,
including while kproxy config edit has an editor open. Direct edits made by a
third-party editor do not voluntarily honor this advisory lock, so prefer
kproxy config edit.
kproxy apikey add generates a random secret by default. With --key, it
preserves the supplied secret verbatim and derives the same stable ID as the
original key, allowing accidental deletion recovery without changing callers.
The value may remain in shell history and process listings, so clean up those
records as appropriate for the environment.
Subscribe to account-credit-protected when accounts should alert after reaching
the scheduler's remaining-credit protection threshold. Same-kind account events
for one target are batched into one message while retaining per-account
once-until-recovery suppression.
Destructive commands have no --yes bypass and require an interactive y or
yes confirmation. Running bare kproxy prints the main help; kproxy help lists
the available topic guides.
kproxy account list sorts by email by default so batch imports are easy to
audit. Use --sort credit or --sort id when those views are needed. Service,
API key, alert-target, and model lists also use stable name or identifier
ordering; logs, recent requests, and pool scores retain their semantic time or
priority order.
kproxy logs show and follow read structured request records retained by the
daemon. kproxy logs trace <TRACE_ID> searches all retained physical severity
and date shards and orders the matching request-chain events by timestamp; add
--level error to restrict it to one exact severity. Physical files use exact
severity partitions: info.log contains only INFO events, while WARN and ERROR
events are stored in warn.log and error.log. kproxy logs files discovers
these shards and prints their sizes and complete paths; kproxy logs path prints
the active directory, base path, format, and filter. When invoked through the
Docker host wrapper, both path commands also report the named volume's real path
on the Docker host. The legacy kproxy logs --tail ... and -f forms remain
supported.
kproxy status reports request, success, credit, and average-latency metrics for
the current daemon session. kproxy stats defaults to persisted cumulative
metrics across restarts. Both commands accept --since 1h or an explicit
timezone-aware RFC 3339 --start/--end range. Time-series aggregates are kept
at one-minute resolution without the previous seven-day eviction. History that
was already evicted before upgrading cannot be recovered; the CLI reports the
earliest available time. The cumulative summary stays compact in stats.json;
minute history is stored in bounded UTC hourly files under stats-history/, and
range parsing/aggregation runs outside the proxy request lock. Use
kproxy stats --detail for recent requests and
grouped counters, and kproxy logs plus trace IDs for individual failures.
Dynamic model discovery runs immediately at daemon startup, again when accounts change, and thereafter when the model-cache TTL expires. The one-minute account status task refreshes usage only and does not duplicate model-list requests.
The default kproxyd build and Docker Compose enable all features, including the
Chromium-based enterprise IAM Identity Center login. First set a global start
URL with kproxy config edit:
[sso]
start_url = "https://example.awsapps.com/start"Manual account additions can then omit --start-url:
printf '%s\n' "$PASSWORD" | kproxy account add-sso \
--email user@example.com \
--password-stdin
kproxy account add-sso --batch accounts.csv -c 1
# Read explicitly from stdin for pipelines and automation:
kproxy account add-sso --batch - -c 1 < accounts.csvUse --start-url to override the global value for one login. When a smaller
binary without browser SSO is explicitly desired, build with
cargo build --workspace --no-default-features or select Docker's
runtime-slim target.
The Docker host wrapper automatically recognizes a readable host CSV and
streams it into the container through stdin, without copying or retaining a
password file. A container path is still read normally when no host file with
the same name exists. Passwords are accepted only from stdin or a two-column
CSV file. Add
--headful when MFA or an upstream page change requires manual interaction.
Every login uses a dedicated incognito Chromium context and temporary profile,
which are destroyed before the next account is processed. Before saving an
account, kproxy records Kiro's stable user ID and refuses to register that same
identity under another email. Kiro display names are diagnostic only because
IAM Identity Center names do not always match login email addresses.
This flow does not add support for non-enterprise or non-SSO accounts.
# Production: pull while the old container is still serving, then replace it.
KPROXY_IMAGE=ghcr.io/yaocool/kiro-proxy:v0.1.3 docker compose pull kproxyd
KPROXY_IMAGE=ghcr.io/yaocool/kiro-proxy:v0.1.3 docker compose up -d --no-build
# Local source build through the explicit build override:
docker compose -f docker-compose.yml -f docker-compose.build.yml up -d --build
# The standalone default is also the full image:
docker build -t kiro-proxy:latest .
# Select slim only when browser SSO is not needed:
docker build --target runtime-slim -t kiro-proxy:slim .
docker build --target runtime-full -t kiro-proxy:full .Compose uses host networking so every manually created proxy service is
available on the Docker host immediately, including custom ports; no Compose
edit or container restart is needed. Services bind to 0.0.0.0 by default, so
restrict proxy ports with the host firewall or cloud security group. Use
--host 127.0.0.1 when host-only access is desired. Docker Engine on Linux
supports host networking directly; Docker Desktop 4.34+ requires it to be
enabled in Settings. State persists in the kproxy-data volume. The full image
adds Chromium for enterprise SSO authentication. Its browser is pinned to the
official r1566079 snapshot used by the CDP definitions in chromiumoxide 0.9.1, so a routine image rebuild cannot silently upgrade the protocol. Update
and test both pins together when adopting browser security updates.
Local and CI builds reuse persistent Cargo registry and target caches. Full-image
builds compile the all-features binaries only once and serialize that release
build with the initial Chromium installation. Local Cargo parallelism defaults
to one job; override it only when the build host has enough memory, for example
CARGO_BUILD_JOBS=4 docker compose -f docker-compose.yml -f docker-compose.build.yml build.
The Build and publish Docker image GitHub Actions workflow runs only when a
v* tag is pushed. A version tag such as v0.1.3 publishes v0.1.3, v0.1,
and latest; the tag must match the Cargo workspace version. After bumping that
version and merging the release commit, publish it with
git tag v0.1.3 && git push origin v0.1.3. Production upgrades pull the new image
before replacing the container and keep the named volume:
./deploy/docker-setup.sh --image ghcr.io/yaocool/kiro-proxy:v0.1.3
docker compose exec kproxyd kproxy config show --effectiveAlternatively, follow the newest stable release automatically with
./deploy/docker-upgrade.sh. Set KPROXY_UPGRADE_IMAGE to use a different
registry image.
The script retains the previous local image under a rollback tag. If container creation or the health check fails, it recreates the service from that image. With host networking, the old and new containers cannot bind the same proxy ports concurrently, so the final container switch still has a short restart window; image download and compilation no longer consume production downtime.
Existing config.toml files are never overwritten. A volume created by an
older version may still contain server.host = "127.0.0.1"; change it with
kproxy config edit if the new 0.0.0.0 default is desired.
Pull, start, and install the Docker-backed wrapper in one step:
./deploy/docker-setup.sh
kproxy health
kproxy service listThe wrapper discovers the running daemon through its Compose label and always
uses the CLI version bundled with that container. It requires permission to use
Docker. Set KPROXY_COMPOSE_PROJECT when multiple kiro-proxy Compose projects are
running. To reinstall only the wrapper, run
sudo ./deploy/install-kproxy-wrapper.sh.
A hardened service template is available at
deploy/kproxyd.service. Install kproxyd and kproxy under
/usr/local/bin, create the kproxy system user and group, install the unit, and
then enable the service. Browser SSO additionally requires Chrome or Chromium on
the host; the provided unit keeps user namespaces and executable JIT memory
available for Chromium while retaining the other process hardening controls.
cargo fmt --all -- --check
cargo clippy --workspace --all-targets --all-features --locked -- -D warnings
cargo test --workspace --all-features --lockedThe project uses Rust edition 2021 with MSRV 1.97.1.
For complete setup, startup, deployment, logging, LLDB, and troubleshooting guidance, see Setup, startup, and debugging.
MIT