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
75 changes: 71 additions & 4 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@
# Example: "my-super-secret-password-123" or any secure string
PROXY_API_KEY="my-super-secret-password-123"

# Disable proxy authentication entirely (for trusted local networks only)
# When true, the proxy will skip API key validation, allowing unauthenticated access.
# Useful when local clients cannot send custom Authorization headers.
# Default: false
# PROXY_AUTH_DISABLED=false

# ===========================================
# OPTION 1: Kiro IDE credentials (JSON file)
# ===========================================
Expand Down Expand Up @@ -56,6 +62,10 @@ PROXY_API_KEY="my-super-secret-password-123"
# For kiro-cli (AWS SSO / Builder ID): not needed, will be ignored
# PROFILE_ARN="arn:aws:codewhisperer:us-east-1:..."

# Custom path to Kiro IDE profile.json (auto-detected by default)
# Useful if Kiro stores profiles in a non-standard location
# KIRO_PROFILE_FILE="~/Library/Application Support/Kiro/User/globalStorage/kiro.kiroagent/profile.json"

# ===========================================
# OPTIONAL
# ===========================================
Expand Down Expand Up @@ -178,9 +188,10 @@ PROXY_API_KEY="my-super-secret-password-123"
# Timeout for waiting for the first token from the model (in seconds).
# If the model doesn't respond within this time, the request will be cancelled and retried.
# This helps handle "stuck" requests when the model takes too long to start responding.
# Default: 15 seconds (recommended for production)
# Set a lower value (e.g., 5-10) for more aggressive retry behavior.
# FIRST_TOKEN_TIMEOUT="15"
# NOTE: Extended thinking models (Opus 4.8/4.7) may need 30-120+ seconds first token.
# Default: 300 seconds (covers extended thinking scenarios)
# Set a lower value (e.g., 15-30) for faster retry on non-thinking models.
# FIRST_TOKEN_TIMEOUT="300"

# Maximum number of retry attempts when first token timeout occurs.
# After exhausting all attempts, a 504 Gateway Timeout error will be returned.
Expand Down Expand Up @@ -241,11 +252,67 @@ PROXY_API_KEY="my-super-secret-password-123"
# Default: 20 characters (enough for longest tag <reasoning> = 11 chars + some whitespace)
# FAKE_REASONING_INITIAL_BUFFER_SIZE=20

# ===========================================
# NATIVE REASONING (Native Kiro Extended Thinking)
# ===========================================

# Enable native reasoning pass-through (default: true)
# When enabled, the Kiro backend's real reasoningContentEvent stream is captured
# and surfaced as reasoning_content (OpenAI) / native thinking blocks (Anthropic).
# The legacy fake-reasoning prompt injection is skipped for these requests.
# Set to false to fall back to fake-reasoning behavior.
# NATIVE_REASONING=true

# Default effort level applied to all requests (when client doesn't specify).
# Valid values: low, medium, high, xhigh, max
# Empty/unset = no default (model decides). Only applies when model supports effort.
# DEFAULT_EFFORT_LEVEL=""

# ===========================================
# NATIVE THINKING (Kiro/Claude Adaptive Thinking)
# ===========================================

# Experimental native adaptive thinking pass-through (default: off)
# Modes:
# - "off": Disabled (preserves existing fake reasoning behavior)
# - "auto": Enable only when the client explicitly requests a reasoning effort
# - "force": Enable for supported models even without client request
# KIRO_NATIVE_THINKING_MODE="off"

# Native thinking display mode (default: summarized)
# - "summarized": Return summarized reasoning text
# - "omitted": Omit thinking text entirely (only signature)
# KIRO_NATIVE_THINKING_DISPLAY="summarized"

# ===========================================
# API HOST OVERRIDE
# ===========================================

# Override the default Kiro API host template (default: https://runtime.{region}.kiro.dev)
# Allows falling back to https://q.{region}.amazonaws.com when runtime.kiro.dev is unavailable
# KIRO_API_HOST_TEMPLATE="https://runtime.{region}.kiro.dev"

# Same as above but for API host template (default: https://runtime.{region}.kiro.dev)
# KIRO_Q_HOST_TEMPLATE="https://runtime.{region}.kiro.dev"

# Complete API host override (takes precedence over templates)
# When set, overrides both API and Q hosts entirely.
# Useful when the default Kiro domain is blocked by corporate proxies.
# KIRO_API_HOST="https://custom.endpoint.com"

# ===========================================
# BILLING HEADER STRIPPING
# ===========================================

# Strip Claude Code's per-request billing attribution headers from system prompts
# (default: true). These random per-request headers invalidate prompt caches.
# STRIP_BILLING_HEADER=true

# ===========================================
# WEBSEARCH (MCP Tool Emulation)
# ===========================================

# Enable web_search tool auto-injection (default: true)
# Enable web_search tool auto-injection (default: false)
# When enabled, web_search is automatically added as a tool for MCP emulation (Path B)
# Model decides whether to use it or not
#
Expand Down
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,4 @@ requests/
# Testing
.pytest_cache/
.coverage
htmlcov/
errors.log
Empty file added extensions/__init__.py
Empty file.
213 changes: 213 additions & 0 deletions extensions/tool_name_alias.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
"""
Runtime tool-name aliasing for Kiro's 64-character tool name limit.

This module lives outside the upstream kiro/ tree. main.py installs it at
startup so kiro-gateway can keep upstream files sync-friendly while still
handling long MCP tool names (e.g. names emitted by aggregating MCP gateways
that exceed Kiro's 64-character limit).

The install hook patches a small surface in kiro.{converters_core,parsers,
streaming_*} to:

- Replace strict >64-char validation with silent registration.
- Rewrite tool names client→Kiro to a stable hashed alias on the way in.
- Rewrite tool names Kiro→client back to the original on the way out
(both stream events and bracket-formatted tool calls).

All replacements are idempotent and reversible via uninstall_tool_name_aliasing,
which is intended for unit tests.
"""

import hashlib
import re
from typing import Any, AsyncGenerator, Dict, Iterable, Optional

from loguru import logger


MAX_KIRO_TOOL_NAME_LENGTH = 64
_KIRO_TOOL_NAME_RE = re.compile(r"^[A-Za-z0-9_-]{1,64}$")
_SAFE_SUFFIX_RE = re.compile(r"[^A-Za-z0-9_-]+")

_alias_to_original: Dict[str, str] = {}
_original_to_alias: Dict[str, str] = {}
_reserved_kiro_names = set()
_installed = False
_originals: Dict[str, Any] = {}


def needs_alias(name: str) -> bool:
return not bool(_KIRO_TOOL_NAME_RE.match(name or ""))


def _build_alias(name: str, digest_len: int) -> str:
digest = hashlib.sha256(name.encode("utf-8")).hexdigest()[:digest_len]
suffix_source = name.rsplit("__", 1)[-1] or name
suffix = _SAFE_SUFFIX_RE.sub("_", suffix_source).strip("_-") or "tool"
prefix = f"t_{digest}_"
max_suffix_len = MAX_KIRO_TOOL_NAME_LENGTH - len(prefix)
return prefix + suffix[-max_suffix_len:]


def alias_for_tool_name(name: str) -> str:
"""Return the Kiro-facing name for a client-facing tool name."""
if not needs_alias(name):
return name

if name in _original_to_alias:
return _original_to_alias[name]

alias = _build_alias(name, 12)
digest_len = 16
while (
alias in _reserved_kiro_names
or (alias in _alias_to_original and _alias_to_original[alias] != name)
):
alias = _build_alias(name, digest_len)
digest_len += 4

_original_to_alias[name] = alias
_alias_to_original[alias] = name
logger.debug(f"Aliased long Kiro tool name: '{name}' -> '{alias}'")
return alias


def original_for_tool_name(name: str) -> str:
"""Return the client-facing name for a Kiro-facing alias."""
return _alias_to_original.get(name, name)


def _register_tool_names(tools: Optional[Iterable[Any]]) -> None:
if not tools:
return
for tool in tools:
name = getattr(tool, "name", None)
if isinstance(name, str) and not needs_alias(name):
_reserved_kiro_names.add(name)
for tool in tools:
name = getattr(tool, "name", None)
if isinstance(name, str):
alias_for_tool_name(name)


def _alias_tool_use(tool_use: Dict[str, Any]) -> Dict[str, Any]:
name = tool_use.get("name")
if isinstance(name, str):
tool_use["name"] = alias_for_tool_name(name)
return tool_use


def _restore_tool_call(tool_call: Dict[str, Any]) -> Dict[str, Any]:
func = tool_call.get("function")
if isinstance(func, dict):
name = func.get("name")
if isinstance(name, str):
func["name"] = original_for_tool_name(name)
name = tool_call.get("name")
if isinstance(name, str):
tool_call["name"] = original_for_tool_name(name)
return tool_call


def _restore_event_tool_name(event: Any) -> Any:
if getattr(event, "type", None) == "tool_use" and getattr(event, "tool_use", None):
_restore_tool_call(event.tool_use)
return event


def install_tool_name_aliasing() -> None:
"""Install runtime patches without modifying upstream kiro/ modules."""
global _installed
if _installed:
return

import kiro.converters_core as converters_core
import kiro.parsers as parsers
import kiro.streaming_core as streaming_core
import kiro.streaming_openai as streaming_openai
import kiro.streaming_anthropic as streaming_anthropic

_originals.update(
validate_tool_names=converters_core.validate_tool_names,
convert_tools_to_kiro_format=converters_core.convert_tools_to_kiro_format,
extract_tool_uses_from_message=converters_core.extract_tool_uses_from_message,
parse_bracket_tool_calls=parsers.parse_bracket_tool_calls,
parse_kiro_stream=streaming_core.parse_kiro_stream,
)

def validate_tool_names_with_aliases(tools):
_register_tool_names(tools)

def convert_tools_to_kiro_format_with_aliases(tools):
_register_tool_names(tools)
if not tools:
return _originals["convert_tools_to_kiro_format"](tools)

aliased_tools = []
for tool in tools:
aliased_tools.append(
converters_core.UnifiedTool(
name=alias_for_tool_name(tool.name),
description=tool.description,
input_schema=tool.input_schema,
)
)
return _originals["convert_tools_to_kiro_format"](aliased_tools)

def extract_tool_uses_from_message_with_aliases(content, tool_calls=None):
tool_uses = _originals["extract_tool_uses_from_message"](content, tool_calls)
return [_alias_tool_use(tool_use) for tool_use in tool_uses]

def parse_bracket_tool_calls_with_restore(response_text):
tool_calls = _originals["parse_bracket_tool_calls"](response_text)
return [_restore_tool_call(tool_call) for tool_call in tool_calls]

async def parse_kiro_stream_with_restore(*args, **kwargs) -> AsyncGenerator[Any, None]:
async for event in _originals["parse_kiro_stream"](*args, **kwargs):
yield _restore_event_tool_name(event)

converters_core.validate_tool_names = validate_tool_names_with_aliases
converters_core.convert_tools_to_kiro_format = convert_tools_to_kiro_format_with_aliases
converters_core.extract_tool_uses_from_message = extract_tool_uses_from_message_with_aliases

parsers.parse_bracket_tool_calls = parse_bracket_tool_calls_with_restore
streaming_core.parse_bracket_tool_calls = parse_bracket_tool_calls_with_restore
streaming_openai.parse_bracket_tool_calls = parse_bracket_tool_calls_with_restore
streaming_anthropic.parse_bracket_tool_calls = parse_bracket_tool_calls_with_restore

streaming_core.parse_kiro_stream = parse_kiro_stream_with_restore
streaming_openai.parse_kiro_stream = parse_kiro_stream_with_restore
streaming_anthropic.parse_kiro_stream = parse_kiro_stream_with_restore

_installed = True
logger.info("Installed Kiro tool-name aliasing extension")


def uninstall_tool_name_aliasing() -> None:
"""Restore patched functions. Intended for unit tests."""
global _installed
if _originals:
import kiro.converters_core as converters_core
import kiro.parsers as parsers
import kiro.streaming_core as streaming_core
import kiro.streaming_openai as streaming_openai
import kiro.streaming_anthropic as streaming_anthropic

converters_core.validate_tool_names = _originals["validate_tool_names"]
converters_core.convert_tools_to_kiro_format = _originals["convert_tools_to_kiro_format"]
converters_core.extract_tool_uses_from_message = _originals["extract_tool_uses_from_message"]

parsers.parse_bracket_tool_calls = _originals["parse_bracket_tool_calls"]
streaming_core.parse_bracket_tool_calls = _originals["parse_bracket_tool_calls"]
streaming_openai.parse_bracket_tool_calls = _originals["parse_bracket_tool_calls"]
streaming_anthropic.parse_bracket_tool_calls = _originals["parse_bracket_tool_calls"]

streaming_core.parse_kiro_stream = _originals["parse_kiro_stream"]
streaming_openai.parse_kiro_stream = _originals["parse_kiro_stream"]
streaming_anthropic.parse_kiro_stream = _originals["parse_kiro_stream"]

_originals.clear()
_alias_to_original.clear()
_original_to_alias.clear()
_reserved_kiro_names.clear()
_installed = False
50 changes: 50 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
set -euo pipefail

# Kiro Gateway - Install/Reinstall Script
# Installs as a user systemd service and (re)starts it.
# Requires: uv (https://docs.astral.sh/uv/)

REPO_DIR="$(cd "$(dirname "$0")" && pwd)"
SERVICE_NAME="kiro-gateway"
SERVICE_FILE="$HOME/.config/systemd/user/${SERVICE_NAME}.service"
VENV_DIR="${REPO_DIR}/.venv"

echo "==> Kiro Gateway installer"
echo " Repo: ${REPO_DIR}"

# Ensure systemd user directory exists
mkdir -p "$HOME/.config/systemd/user"

# Create/update virtualenv and install dependencies
echo "==> Installing dependencies with uv..."
uv venv "$VENV_DIR" --quiet
uv pip install --quiet -r "${REPO_DIR}/requirements.txt" -p "${VENV_DIR}/bin/python"

# Write systemd service unit
echo "==> Writing systemd service to ${SERVICE_FILE}"
cat > "$SERVICE_FILE" <<EOF
[Unit]
Description=Kiro Gateway
After=network.target

[Service]
WorkingDirectory=${REPO_DIR}
ExecStart=${VENV_DIR}/bin/python main.py
Restart=on-failure
RestartSec=5
Environment=PATH=${VENV_DIR}/bin:/usr/local/bin:/usr/bin:/bin

[Install]
WantedBy=default.target
EOF

# Reload, enable, and restart
echo "==> Reloading systemd and restarting ${SERVICE_NAME}..."
systemctl --user daemon-reload
systemctl --user enable "${SERVICE_NAME}.service"
systemctl --user restart "${SERVICE_NAME}.service"

# Show status
echo "==> Done. Service status:"
systemctl --user status "${SERVICE_NAME}.service" --no-pager
Loading