From 76a322ba337522361f51898b5bfd9cb9f56d34ff Mon Sep 17 00:00:00 2001 From: AXGZ21 Date: Sat, 14 Feb 2026 22:23:26 +0100 Subject: [PATCH] feat: add OpenAI-compatible endpoints per agent --- README.md | 27 ++++++++- docs/AGENTS.md | 21 +++++-- lib/agents.sh | 52 ++++++++++++---- lib/setup-wizard.sh | 141 ++++++++++++++++++++++++++++---------------- package-lock.json | 4 +- src/lib/config.ts | 37 +++++++++++- src/lib/invoke.ts | 34 ++++++++++- src/lib/types.ts | 6 ++ 8 files changed, 243 insertions(+), 79 deletions(-) diff --git a/README.md b/README.md index 3362ef0..f70108a 100644 --- a/README.md +++ b/README.md @@ -77,8 +77,9 @@ The setup wizard will guide you through: 3. **Workspace setup** - Name your workspace directory 4. **Default agent** - Configure your main AI assistant 5. **AI provider** - Select Anthropic (Claude) or OpenAI -6. **Model selection** - Choose model (e.g., Sonnet, Opus, GPT-5.3) -7. **Heartbeat interval** - Set proactive check-in frequency +6. **Model selection** - Choose model (e.g., Sonnet, Opus, GPT-5.3, or custom OpenAI-compatible model) +7. **OpenAI endpoint (optional)** - Set custom OpenAI-compatible `base_url` and `api_key` +8. **Heartbeat interval** - Set proactive check-in frequency
📱 Channel Setup Guides @@ -277,7 +278,11 @@ Agents are configured in `.tinyclaw/settings.json`: "name": "Technical Writer", "provider": "openai", "model": "gpt-5.3-codex", - "working_directory": "/Users/me/tinyclaw-workspace/writer" + "working_directory": "/Users/me/tinyclaw-workspace/writer", + "openai": { + "base_url": "https://openrouter.ai/api/v1", + "api_key": "your-api-key" + } } } } @@ -289,6 +294,7 @@ Each agent operates in isolation: - **Own conversation history** - Maintained by CLI - **Custom configuration** - `.claude/`, `heartbeat.md` (root), `AGENTS.md` - **Independent resets** - Reset individual agent conversations +- **Optional OpenAI-compatible endpoints** - Set `openai.base_url`/`openai.api_key` per OpenAI agent
📖 Learn more about agents @@ -431,6 +437,21 @@ Located at `.tinyclaw/settings.json`: } ``` +For OpenAI-compatible providers, you can also configure endpoint defaults in `models.openai` (applies to default/fallback agent and as defaults for OpenAI agents): + +```json +{ + "models": { + "provider": "openai", + "openai": { + "model": "gpt-5.3-codex", + "base_url": "https://openrouter.ai/api/v1", + "api_key": "your-api-key" + } + } +} +``` + ### Heartbeat Configuration Edit agent-specific heartbeat prompts: diff --git a/docs/AGENTS.md b/docs/AGENTS.md index 793e91e..cabad17 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -97,6 +97,10 @@ Each agent has its own configuration in `.tinyclaw/settings.json`: "provider": "openai", "model": "gpt-5.3-codex", "working_directory": "/Users/me/tinyclaw-workspace/writer", + "openai": { + "base_url": "https://openrouter.ai/api/v1", + "api_key": "your-api-key" + }, "prompt_file": "/path/to/writer-prompt.md" }, "assistant": { @@ -182,6 +186,8 @@ claude --dangerously-skip-permissions \ **OpenAI (Codex):** ```bash cd "$agent_working_directory" # e.g., ~/tinyclaw-workspace/coder/ +OPENAI_BASE_URL="https://openrouter.ai/api/v1" \ +OPENAI_API_KEY="your-api-key" \ codex exec resume --last \ --model gpt-5.3-codex \ --skip-git-repo-check \ @@ -189,6 +195,7 @@ codex exec resume --last \ --json \ "User message here" ``` +`OPENAI_BASE_URL` and `OPENAI_API_KEY` are optional and only used when configured. ## Configuration @@ -215,8 +222,8 @@ This walks you through: 1. Agent ID (e.g., `coder`) 2. Display name (e.g., `Code Assistant`) 3. Provider (Anthropic or OpenAI) -4. Model selection -5. Optional system prompt +4. Model selection (including custom OpenAI-compatible model names) +5. Optional OpenAI-compatible endpoint settings (`base_url`, `api_key`) for OpenAI agents **Working directory is automatically set to:** `//` @@ -250,6 +257,8 @@ Edit `.tinyclaw/settings.json`: | `provider` | Yes | `anthropic` or `openai` | | `model` | Yes | Model identifier (e.g., `sonnet`, `opus`, `gpt-5.3-codex`) | | `working_directory` | Yes | Directory where agent operates (auto-set to `//`) | +| `openai.base_url` | No | Optional OpenAI-compatible endpoint base URL for OpenAI agents | +| `openai.api_key` | No | Optional API key exported as `OPENAI_API_KEY` for OpenAI agents | | `system_prompt` | No | Inline system prompt text | | `prompt_file` | No | Path to file containing system prompt | @@ -428,9 +437,11 @@ If no agents are configured, TinyClaw automatically creates a default agent usin ```json { "models": { - "provider": "anthropic", - "anthropic": { - "model": "sonnet" + "provider": "openai", + "openai": { + "model": "gpt-5.3-codex", + "base_url": "https://openrouter.ai/api/v1", + "api_key": "your-api-key" } } } diff --git a/lib/agents.sh b/lib/agents.sh index 36ec61a..aa80b2f 100644 --- a/lib/agents.sh +++ b/lib/agents.sh @@ -28,10 +28,13 @@ agent_list() { echo "=================" echo "" - jq -r '(.agents // {}) | to_entries[] | "\(.key)|\(.value.name)|\(.value.provider)|\(.value.model)|\(.value.working_directory)"' "$SETTINGS_FILE" 2>/dev/null | \ - while IFS='|' read -r id name provider model workdir; do + jq -r '(.agents // {}) | to_entries[] | "\(.key)|\(.value.name)|\(.value.provider)|\(.value.model)|\(.value.working_directory)|\(.value.openai.base_url // "")"' "$SETTINGS_FILE" 2>/dev/null | \ + while IFS='|' read -r id name provider model workdir openai_base_url; do echo -e " ${GREEN}@${id}${NC} - ${name}" echo " Provider: ${provider}/${model}" + if [ -n "$openai_base_url" ]; then + echo " Endpoint: ${openai_base_url}" + fi echo " Directory: ${workdir}" echo "" done @@ -115,6 +118,8 @@ agent_add() { # Model echo "" + AGENT_OPENAI_BASE_URL="" + AGENT_OPENAI_API_KEY="" if [ "$AGENT_PROVIDER" = "anthropic" ]; then echo "Model:" echo " 1) Sonnet (fast)" @@ -128,11 +133,21 @@ agent_add() { echo "Model:" echo " 1) GPT-5.3 Codex" echo " 2) GPT-5.2" - read -rp "Choose [1-2, default: 1]: " AGENT_MODEL_CHOICE + echo " 3) Custom model name" + read -rp "Choose [1-3, default: 1]: " AGENT_MODEL_CHOICE case "$AGENT_MODEL_CHOICE" in 2) AGENT_MODEL="gpt-5.2" ;; + 3) + read -rp "Enter OpenAI-compatible model name: " AGENT_MODEL + if [ -z "$AGENT_MODEL" ]; then + echo -e "${RED}Model name cannot be empty${NC}" + exit 1 + fi + ;; *) AGENT_MODEL="gpt-5.3-codex" ;; esac + read -rp "OpenAI-compatible base URL (optional): " AGENT_OPENAI_BASE_URL + read -rp "OpenAI API key (optional, saved in settings.json): " AGENT_OPENAI_API_KEY fi # Working directory - automatically set to agent directory @@ -148,12 +163,23 @@ agent_add() { --arg provider "$AGENT_PROVIDER" \ --arg model "$AGENT_MODEL" \ --arg workdir "$AGENT_WORKDIR" \ + --arg base_url "$AGENT_OPENAI_BASE_URL" \ + --arg api_key "$AGENT_OPENAI_API_KEY" \ '{ name: $name, provider: $provider, model: $model, working_directory: $workdir - }') + } | + if $provider == "openai" and ($base_url != "" or $api_key != "") then + .openai = ( + {} + + (if $base_url != "" then {base_url: $base_url} else {} end) + + (if $api_key != "" then {api_key: $api_key} else {} end) + ) + else + . + end') # Ensure agents section exists and add the new agent jq --arg id "$AGENT_ID" --argjson agent "$agent_json" \ @@ -171,7 +197,7 @@ agent_add() { # Copy .claude directory if [ -d "$SCRIPT_DIR/.claude" ]; then cp -r "$SCRIPT_DIR/.claude" "$AGENTS_DIR/$AGENT_ID/" - echo " → Copied .claude/ to agent directory" + echo " -> Copied .claude/ to agent directory" else mkdir -p "$AGENTS_DIR/$AGENT_ID/.claude" fi @@ -179,36 +205,36 @@ agent_add() { # Copy heartbeat.md if [ -f "$SCRIPT_DIR/heartbeat.md" ]; then cp "$SCRIPT_DIR/heartbeat.md" "$AGENTS_DIR/$AGENT_ID/" - echo " → Copied heartbeat.md to agent directory" + echo " -> Copied heartbeat.md to agent directory" fi # Copy AGENTS.md if [ -f "$SCRIPT_DIR/AGENTS.md" ]; then cp "$SCRIPT_DIR/AGENTS.md" "$AGENTS_DIR/$AGENT_ID/" - echo " → Copied AGENTS.md to agent directory" + echo " -> Copied AGENTS.md to agent directory" fi # Copy AGENTS.md content into .claude/CLAUDE.md as well if [ -f "$SCRIPT_DIR/AGENTS.md" ]; then cp "$SCRIPT_DIR/AGENTS.md" "$AGENTS_DIR/$AGENT_ID/.claude/CLAUDE.md" - echo " → Copied CLAUDE.md to .claude/ directory" + echo " -> Copied CLAUDE.md to .claude/ directory" fi # Symlink skills directory into .claude/skills if [ -d "$SCRIPT_DIR/.agents/skills" ] && [ ! -e "$AGENTS_DIR/$AGENT_ID/.claude/skills" ]; then ln -s "$SCRIPT_DIR/.agents/skills" "$AGENTS_DIR/$AGENT_ID/.claude/skills" - echo " → Linked skills to .claude/skills/" + echo " -> Linked skills to .claude/skills/" fi # Create .tinyclaw directory and copy SOUL.md mkdir -p "$AGENTS_DIR/$AGENT_ID/.tinyclaw" if [ -f "$SCRIPT_DIR/SOUL.md" ]; then cp "$SCRIPT_DIR/SOUL.md" "$AGENTS_DIR/$AGENT_ID/.tinyclaw/SOUL.md" - echo " → Copied SOUL.md to .tinyclaw/" + echo " -> Copied SOUL.md to .tinyclaw/" fi echo "" - echo -e "${GREEN}✓ Agent '${AGENT_ID}' created!${NC}" + echo -e "${GREEN}[OK] Agent '${AGENT_ID}' created!${NC}" echo -e " Directory: $AGENTS_DIR/$AGENT_ID" echo "" echo -e "${BLUE}Next steps:${NC}" @@ -253,7 +279,7 @@ agent_remove() { rm -rf "$AGENTS_DIR/$agent_id" fi - echo -e "${GREEN}✓ Agent '${agent_id}' removed.${NC}" + echo -e "${GREEN}[OK] Agent '${agent_id}' removed.${NC}" } # Reset a specific agent's conversation @@ -279,7 +305,7 @@ agent_reset() { local agent_name agent_name=$(jq -r "(.agents // {}).\"${agent_id}\".name" "$SETTINGS_FILE" 2>/dev/null) - echo -e "${GREEN}✓ Reset flag set for agent '${agent_id}' (${agent_name})${NC}" + echo -e "${GREEN}[OK] Reset flag set for agent '${agent_id}' (${agent_name})${NC}" echo "" echo "The next message to @${agent_id} will start a fresh conversation." } diff --git a/lib/setup-wizard.sh b/lib/setup-wizard.sh index c66abb7..0764345 100755 --- a/lib/setup-wizard.sh +++ b/lib/setup-wizard.sh @@ -12,13 +12,12 @@ BLUE='\033[0;34m' NC='\033[0m' echo "" -echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo -e "${BLUE}============================================================${NC}" echo -e "${GREEN} TinyClaw - Setup Wizard${NC}" -echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo -e "${BLUE}============================================================${NC}" echo "" # --- Channel registry --- -# To add a new channel, add its ID here and fill in the config arrays below. ALL_CHANNELS=(telegram discord whatsapp) declare -A CHANNEL_DISPLAY=( @@ -39,7 +38,6 @@ declare -A CHANNEL_TOKEN_HELP=( [telegram]="(Create a bot via @BotFather on Telegram to get a token)" ) -# Channel selection - simple checklist echo "Which messaging channels (Telegram, Discord, WhatsApp) do you want to enable?" echo "" @@ -48,7 +46,7 @@ for ch in "${ALL_CHANNELS[@]}"; do read -rp " Enable ${CHANNEL_DISPLAY[$ch]}? [y/N]: " choice if [[ "$choice" =~ ^[yY] ]]; then ENABLED_CHANNELS+=("$ch") - echo -e " ${GREEN}✓ ${CHANNEL_DISPLAY[$ch]} enabled${NC}" + echo -e " ${GREEN}[OK] ${CHANNEL_DISPLAY[$ch]} enabled${NC}" fi done echo "" @@ -73,12 +71,11 @@ for ch in "${ENABLED_CHANNELS[@]}"; do exit 1 fi TOKENS[$ch]="$token_value" - echo -e "${GREEN}✓ ${CHANNEL_DISPLAY[$ch]} token saved${NC}" + echo -e "${GREEN}[OK] ${CHANNEL_DISPLAY[$ch]} token saved${NC}" echo "" fi done -# Provider selection echo "Which AI provider?" echo "" echo " 1) Anthropic (Claude) (recommended)" @@ -94,10 +91,12 @@ case "$PROVIDER_CHOICE" in exit 1 ;; esac -echo -e "${GREEN}✓ Provider: $PROVIDER${NC}" +echo -e "${GREEN}[OK] Provider: $PROVIDER${NC}" echo "" # Model selection based on provider +OPENAI_BASE_URL="" +OPENAI_API_KEY="" if [ "$PROVIDER" = "anthropic" ]; then echo "Which Claude model?" echo "" @@ -114,30 +113,40 @@ if [ "$PROVIDER" = "anthropic" ]; then exit 1 ;; esac - echo -e "${GREEN}✓ Model: $MODEL${NC}" + echo -e "${GREEN}[OK] Model: $MODEL${NC}" echo "" else - # OpenAI models echo "Which OpenAI model?" echo "" echo " 1) GPT-5.3 Codex (recommended)" echo " 2) GPT-5.2" + echo " 3) Custom model name" echo "" - read -rp "Choose [1-2]: " MODEL_CHOICE + read -rp "Choose [1-3]: " MODEL_CHOICE case "$MODEL_CHOICE" in 1) MODEL="gpt-5.3-codex" ;; 2) MODEL="gpt-5.2" ;; + 3) + read -rp "Enter OpenAI-compatible model name: " MODEL + if [ -z "$MODEL" ]; then + echo -e "${RED}Model name cannot be empty${NC}" + exit 1 + fi + ;; *) echo -e "${RED}Invalid choice${NC}" exit 1 ;; esac - echo -e "${GREEN}✓ Model: $MODEL${NC}" + echo -e "${GREEN}[OK] Model: $MODEL${NC}" + echo "" + + read -rp "OpenAI-compatible base URL (optional, e.g. https://api.openai.com/v1): " OPENAI_BASE_URL + read -rp "OpenAI API key (optional, saved in settings.json): " OPENAI_API_KEY echo "" fi -# Heartbeat interval echo "Heartbeat interval (seconds)?" echo -e "${YELLOW}(How often Claude checks in proactively)${NC}" echo "" @@ -148,36 +157,31 @@ if ! [[ "$HEARTBEAT_INTERVAL" =~ ^[0-9]+$ ]]; then echo -e "${RED}Invalid interval, using default 3600${NC}" HEARTBEAT_INTERVAL=3600 fi -echo -e "${GREEN}✓ Heartbeat interval: ${HEARTBEAT_INTERVAL}s${NC}" +echo -e "${GREEN}[OK] Heartbeat interval: ${HEARTBEAT_INTERVAL}s${NC}" echo "" -# Workspace configuration echo "Workspace name (where agent directories will be stored)?" echo -e "${YELLOW}(Creates ~/your-workspace-name/)${NC}" echo "" read -rp "Workspace name [default: tinyclaw-workspace]: " WORKSPACE_INPUT WORKSPACE_NAME=${WORKSPACE_INPUT:-tinyclaw-workspace} -# Clean workspace name WORKSPACE_NAME=$(echo "$WORKSPACE_NAME" | tr ' ' '-' | tr -cd 'a-zA-Z0-9_-') WORKSPACE_PATH="$HOME/$WORKSPACE_NAME" -echo -e "${GREEN}✓ Workspace: $WORKSPACE_PATH${NC}" +echo -e "${GREEN}[OK] Workspace: $WORKSPACE_PATH${NC}" echo "" -# Default agent name echo "Name your default agent?" echo -e "${YELLOW}(The main AI assistant you'll interact with)${NC}" echo "" read -rp "Default agent name [default: assistant]: " DEFAULT_AGENT_INPUT DEFAULT_AGENT_NAME=${DEFAULT_AGENT_INPUT:-assistant} -# Clean agent name DEFAULT_AGENT_NAME=$(echo "$DEFAULT_AGENT_NAME" | tr ' ' '-' | tr -cd 'a-zA-Z0-9_-' | tr '[:upper:]' '[:lower:]') -echo -e "${GREEN}✓ Default agent: $DEFAULT_AGENT_NAME${NC}" +echo -e "${GREEN}[OK] Default agent: $DEFAULT_AGENT_NAME${NC}" echo "" -# --- Additional Agents (optional) --- -echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo -e "${BLUE}============================================================${NC}" echo -e "${GREEN} Additional Agents (Optional)${NC}" -echo -e "${BLUE}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" +echo -e "${BLUE}============================================================${NC}" echo "" echo "You can set up multiple agents with different roles, models, and working directories." echo "Users route messages with '@agent_id message' in chat." @@ -185,18 +189,30 @@ echo "" read -rp "Set up additional agents? [y/N]: " SETUP_AGENTS AGENTS_JSON="" -# Always create the default agent DEFAULT_AGENT_DIR="$WORKSPACE_PATH/$DEFAULT_AGENT_NAME" -# Capitalize first letter of agent name (proper bash method) DEFAULT_AGENT_DISPLAY="$(tr '[:lower:]' '[:upper:]' <<< "${DEFAULT_AGENT_NAME:0:1}")${DEFAULT_AGENT_NAME:1}" AGENTS_JSON='"agents": {' -AGENTS_JSON="$AGENTS_JSON \"$DEFAULT_AGENT_NAME\": { \"name\": \"$DEFAULT_AGENT_DISPLAY\", \"provider\": \"$PROVIDER\", \"model\": \"$MODEL\", \"working_directory\": \"$DEFAULT_AGENT_DIR\" }" -ADDITIONAL_AGENTS=() # Track additional agent IDs for directory creation +DEFAULT_OPENAI_JSON="" +if [ "$PROVIDER" = "openai" ] && { [ -n "$OPENAI_BASE_URL" ] || [ -n "$OPENAI_API_KEY" ]; }; then + DEFAULT_OPENAI_JSON=', "openai": {' + if [ -n "$OPENAI_BASE_URL" ]; then + DEFAULT_OPENAI_JSON="$DEFAULT_OPENAI_JSON \"base_url\": \"$OPENAI_BASE_URL\"" + fi + if [ -n "$OPENAI_API_KEY" ]; then + if [ -n "$OPENAI_BASE_URL" ]; then + DEFAULT_OPENAI_JSON="$DEFAULT_OPENAI_JSON, " + fi + DEFAULT_OPENAI_JSON="$DEFAULT_OPENAI_JSON \"api_key\": \"$OPENAI_API_KEY\"" + fi + DEFAULT_OPENAI_JSON="$DEFAULT_OPENAI_JSON }" +fi -if [[ "$SETUP_AGENTS" =~ ^[yY] ]]; then +AGENTS_JSON="$AGENTS_JSON \"$DEFAULT_AGENT_NAME\": { \"name\": \"$DEFAULT_AGENT_DISPLAY\", \"provider\": \"$PROVIDER\", \"model\": \"$MODEL\", \"working_directory\": \"$DEFAULT_AGENT_DIR\"${DEFAULT_OPENAI_JSON} }" - # Add more agents +ADDITIONAL_AGENTS=() + +if [[ "$SETUP_AGENTS" =~ ^[yY] ]]; then ADDING_AGENTS=true while [ "$ADDING_AGENTS" = true ]; do echo "" @@ -223,6 +239,9 @@ if [[ "$SETUP_AGENTS" =~ ^[yY] ]]; then *) NEW_PROVIDER="anthropic" ;; esac + NEW_OPENAI_BASE_URL="" + NEW_OPENAI_API_KEY="" + if [ "$NEW_PROVIDER" = "anthropic" ]; then echo " Model: 1) Sonnet 2) Opus" read -rp " Choose [1-2, default: 1]: " NEW_MODEL_CHOICE @@ -231,22 +250,44 @@ if [[ "$SETUP_AGENTS" =~ ^[yY] ]]; then *) NEW_MODEL="sonnet" ;; esac else - echo " Model: 1) GPT-5.3 Codex 2) GPT-5.2" - read -rp " Choose [1-2, default: 1]: " NEW_MODEL_CHOICE + echo " Model: 1) GPT-5.3 Codex 2) GPT-5.2 3) Custom model name" + read -rp " Choose [1-3, default: 1]: " NEW_MODEL_CHOICE case "$NEW_MODEL_CHOICE" in 2) NEW_MODEL="gpt-5.2" ;; + 3) + read -rp " Enter model name: " NEW_MODEL + if [ -z "$NEW_MODEL" ]; then + echo -e "${RED} Model name cannot be empty, skipping${NC}" + continue + fi + ;; *) NEW_MODEL="gpt-5.3-codex" ;; esac + + read -rp " OpenAI-compatible base URL (optional): " NEW_OPENAI_BASE_URL + read -rp " OpenAI API key (optional, saved in settings.json): " NEW_OPENAI_API_KEY fi NEW_AGENT_DIR="$WORKSPACE_PATH/$NEW_AGENT_ID" + NEW_OPENAI_JSON="" + if [ "$NEW_PROVIDER" = "openai" ] && { [ -n "$NEW_OPENAI_BASE_URL" ] || [ -n "$NEW_OPENAI_API_KEY" ]; }; then + NEW_OPENAI_JSON=', "openai": {' + if [ -n "$NEW_OPENAI_BASE_URL" ]; then + NEW_OPENAI_JSON="$NEW_OPENAI_JSON \"base_url\": \"$NEW_OPENAI_BASE_URL\"" + fi + if [ -n "$NEW_OPENAI_API_KEY" ]; then + if [ -n "$NEW_OPENAI_BASE_URL" ]; then + NEW_OPENAI_JSON="$NEW_OPENAI_JSON, " + fi + NEW_OPENAI_JSON="$NEW_OPENAI_JSON \"api_key\": \"$NEW_OPENAI_API_KEY\"" + fi + NEW_OPENAI_JSON="$NEW_OPENAI_JSON }" + fi - AGENTS_JSON="$AGENTS_JSON, \"$NEW_AGENT_ID\": { \"name\": \"$NEW_AGENT_NAME\", \"provider\": \"$NEW_PROVIDER\", \"model\": \"$NEW_MODEL\", \"working_directory\": \"$NEW_AGENT_DIR\" }" + AGENTS_JSON="$AGENTS_JSON, \"$NEW_AGENT_ID\": { \"name\": \"$NEW_AGENT_NAME\", \"provider\": \"$NEW_PROVIDER\", \"model\": \"$NEW_MODEL\", \"working_directory\": \"$NEW_AGENT_DIR\"${NEW_OPENAI_JSON} }" - # Track this agent for directory creation later ADDITIONAL_AGENTS+=("$NEW_AGENT_ID") - - echo -e " ${GREEN}✓ Agent '${NEW_AGENT_ID}' added${NC}" + echo -e " ${GREEN}[OK] Agent '${NEW_AGENT_ID}' added${NC}" done fi @@ -262,16 +303,20 @@ for i in "${!ENABLED_CHANNELS[@]}"; do done CHANNELS_JSON="${CHANNELS_JSON}]" -# Build channel configs with tokens DISCORD_TOKEN="${TOKENS[discord]:-}" TELEGRAM_TOKEN="${TOKENS[telegram]:-}" -# Write settings.json with layered structure -# Use jq to build valid JSON to avoid escaping issues with agent prompts if [ "$PROVIDER" = "anthropic" ]; then MODELS_SECTION='"models": { "provider": "anthropic", "anthropic": { "model": "'"${MODEL}"'" } }' else - MODELS_SECTION='"models": { "provider": "openai", "openai": { "model": "'"${MODEL}"'" } }' + MODELS_SECTION='"models": { "provider": "openai", "openai": { "model": "'"${MODEL}"'"' + if [ -n "$OPENAI_BASE_URL" ]; then + MODELS_SECTION="$MODELS_SECTION, \"base_url\": \"$OPENAI_BASE_URL\"" + fi + if [ -n "$OPENAI_API_KEY" ]; then + MODELS_SECTION="$MODELS_SECTION, \"api_key\": \"$OPENAI_API_KEY\"" + fi + MODELS_SECTION="$MODELS_SECTION } }" fi cat > "$SETTINGS_FILE" < "$SETTINGS_FILE" < /dev/null; then tmp_file="$SETTINGS_FILE.tmp" jq '.' "$SETTINGS_FILE" > "$tmp_file" 2>/dev/null && mv "$tmp_file" "$SETTINGS_FILE" fi -# Create workspace directory mkdir -p "$WORKSPACE_PATH" -echo -e "${GREEN}✓ Created workspace: $WORKSPACE_PATH${NC}" +echo -e "${GREEN}[OK] Created workspace: $WORKSPACE_PATH${NC}" -# Create ~/.tinyclaw with templates TINYCLAW_HOME="$HOME/.tinyclaw" mkdir -p "$TINYCLAW_HOME" mkdir -p "$TINYCLAW_HOME/logs" @@ -321,9 +363,8 @@ fi if [ -f "$PROJECT_ROOT/AGENTS.md" ]; then cp "$PROJECT_ROOT/AGENTS.md" "$TINYCLAW_HOME/" fi -echo -e "${GREEN}✓ Created ~/.tinyclaw with templates${NC}" +echo -e "${GREEN}[OK] Created ~/.tinyclaw with templates${NC}" -# Create default agent directory with config files mkdir -p "$DEFAULT_AGENT_DIR" if [ -d "$TINYCLAW_HOME/.claude" ]; then cp -r "$TINYCLAW_HOME/.claude" "$DEFAULT_AGENT_DIR/" @@ -334,13 +375,11 @@ fi if [ -f "$TINYCLAW_HOME/AGENTS.md" ]; then cp "$TINYCLAW_HOME/AGENTS.md" "$DEFAULT_AGENT_DIR/" fi -echo -e "${GREEN}✓ Created default agent directory: $DEFAULT_AGENT_DIR${NC}" +echo -e "${GREEN}[OK] Created default agent directory: $DEFAULT_AGENT_DIR${NC}" -# Create ~/.tinyclaw/files directory for file exchange mkdir -p "$TINYCLAW_HOME/files" -echo -e "${GREEN}✓ Created files directory: $TINYCLAW_HOME/files${NC}" +echo -e "${GREEN}[OK] Created files directory: $TINYCLAW_HOME/files${NC}" -# Create directories for additional agents for agent_id in "${ADDITIONAL_AGENTS[@]}"; do AGENT_DIR="$WORKSPACE_PATH/$agent_id" mkdir -p "$AGENT_DIR" @@ -353,10 +392,10 @@ for agent_id in "${ADDITIONAL_AGENTS[@]}"; do if [ -f "$TINYCLAW_HOME/AGENTS.md" ]; then cp "$TINYCLAW_HOME/AGENTS.md" "$AGENT_DIR/" fi - echo -e "${GREEN}✓ Created agent directory: $AGENT_DIR${NC}" + echo -e "${GREEN}[OK] Created agent directory: $AGENT_DIR${NC}" done -echo -e "${GREEN}✓ Configuration saved to ~/.tinyclaw/settings.json${NC}" +echo -e "${GREEN}[OK] Configuration saved to ~/.tinyclaw/settings.json${NC}" echo "" echo "You can manage agents later with:" echo -e " ${GREEN}tinyclaw agent list${NC} - List agents" diff --git a/package-lock.json b/package-lock.json index 16f032a..2f5c827 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "tinyclaw", - "version": "0.0.1", + "version": "0.0.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "tinyclaw", - "version": "0.0.1", + "version": "0.0.3", "dependencies": { "@types/react": "^19.2.14", "discord.js": "^14.16.0", diff --git a/src/lib/config.ts b/src/lib/config.ts index 65c5b99..f81b456 100644 --- a/src/lib/config.ts +++ b/src/lib/config.ts @@ -44,9 +44,10 @@ export function getSettings(): Settings { */ export function getDefaultAgentFromModels(settings: Settings): AgentConfig { const provider = settings?.models?.provider || 'anthropic'; + const openAISettings = settings?.models?.openai; let model = ''; if (provider === 'openai') { - model = settings?.models?.openai?.model || 'gpt-5.3-codex'; + model = openAISettings?.model || 'gpt-5.3-codex'; } else { model = settings?.models?.anthropic?.model || 'sonnet'; } @@ -55,12 +56,21 @@ export function getDefaultAgentFromModels(settings: Settings): AgentConfig { const workspacePath = settings?.workspace?.path || path.join(require('os').homedir(), 'tinyclaw-workspace'); const defaultAgentDir = path.join(workspacePath, 'default'); - return { + const defaultAgent: AgentConfig = { name: 'Default', provider, model, working_directory: defaultAgentDir, }; + + if (provider === 'openai' && (openAISettings?.base_url || openAISettings?.api_key)) { + defaultAgent.openai = { + ...(openAISettings.base_url ? { base_url: openAISettings.base_url } : {}), + ...(openAISettings.api_key ? { api_key: openAISettings.api_key } : {}), + }; + } + + return defaultAgent; } /** @@ -69,7 +79,28 @@ export function getDefaultAgentFromModels(settings: Settings): AgentConfig { */ export function getAgents(settings: Settings): Record { if (settings.agents && Object.keys(settings.agents).length > 0) { - return settings.agents; + const openAISettings = settings?.models?.openai; + return Object.fromEntries( + Object.entries(settings.agents).map(([id, agent]) => { + if (agent.provider !== 'openai') { + return [id, agent]; + } + + const mergedOpenAI = { + ...(openAISettings?.base_url ? { base_url: openAISettings.base_url } : {}), + ...(openAISettings?.api_key ? { api_key: openAISettings.api_key } : {}), + ...(agent.openai || {}), + }; + + return [ + id, + { + ...agent, + ...(Object.keys(mergedOpenAI).length > 0 ? { openai: mergedOpenAI } : {}), + } + ]; + }) + ); } // Fall back to default agent from models section return { default: getDefaultAgentFromModels(settings) }; diff --git a/src/lib/invoke.ts b/src/lib/invoke.ts index 471eaf2..9beef3d 100644 --- a/src/lib/invoke.ts +++ b/src/lib/invoke.ts @@ -6,11 +6,13 @@ import { SCRIPT_DIR, resolveClaudeModel, resolveCodexModel } from './config'; import { log } from './logging'; import { ensureAgentDirectory, updateAgentTeammates } from './agent-setup'; -export async function runCommand(command: string, args: string[], cwd?: string): Promise { +export async function runCommand(command: string, args: string[], cwd?: string, envOverrides?: NodeJS.ProcessEnv): Promise { return new Promise((resolve, reject) => { + const env = envOverrides ? { ...process.env, ...envOverrides } : process.env; const child = spawn(command, args, { cwd: cwd || SCRIPT_DIR, stdio: ['ignore', 'pipe', 'pipe'], + env, }); let stdout = ''; @@ -43,6 +45,26 @@ export async function runCommand(command: string, args: string[], cwd?: string): }); } +function buildOpenAIEnvOverrides(agent: AgentConfig): NodeJS.ProcessEnv | undefined { + const baseUrl = agent.openai?.base_url?.trim(); + const apiKey = agent.openai?.api_key?.trim(); + + if (!baseUrl && !apiKey) { + return undefined; + } + + const env: NodeJS.ProcessEnv = {}; + if (baseUrl) { + env.OPENAI_BASE_URL = baseUrl; + // Keep alias for CLIs that still look for OPENAI_API_BASE. + env.OPENAI_API_BASE = baseUrl; + } + if (apiKey) { + env.OPENAI_API_KEY = apiKey; + } + return env; +} + /** * Invoke a single agent with a message. Contains all Claude/Codex invocation logic. * Returns the raw response text. @@ -95,7 +117,15 @@ export async function invokeAgent( } codexArgs.push('--skip-git-repo-check', '--dangerously-bypass-approvals-and-sandbox', '--json', message); - const codexOutput = await runCommand('codex', codexArgs, workingDir); + const openAIEnv = buildOpenAIEnvOverrides(agent); + if (openAIEnv?.OPENAI_BASE_URL) { + log('INFO', `Using custom OpenAI-compatible endpoint for agent ${agentId}: ${openAIEnv.OPENAI_BASE_URL}`); + } + if (openAIEnv?.OPENAI_API_KEY) { + log('INFO', `Using custom OpenAI API key for agent ${agentId}`); + } + + const codexOutput = await runCommand('codex', codexArgs, workingDir, openAIEnv); // Parse JSONL output and extract final agent_message let response = ''; diff --git a/src/lib/types.ts b/src/lib/types.ts index 1a20267..348ed5f 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -3,6 +3,10 @@ export interface AgentConfig { provider: string; // 'anthropic' or 'openai' model: string; // e.g. 'sonnet', 'opus', 'gpt-5.3-codex' working_directory: string; + openai?: { + base_url?: string; + api_key?: string; + }; } export interface TeamConfig { @@ -34,6 +38,8 @@ export interface Settings { }; openai?: { model?: string; + base_url?: string; + api_key?: string; }; }; agents?: Record;