feat(claude-profile): add provider selection (model + endpoint + credentials)
Introduces provider files (`data/claude-profile/providers/*.yaml`) that bundle model ID, API base URL, API key source, and extra env vars. Phase 2.5 (provider selection) added between mode selection and launch, with a default derived from the mode's driver field so existing Anthropic usage is unchanged. Ships 5 providers: anthropic-haiku/sonnet/opus/opus-1m and minimax-sonnet. MiniMax token decrypted from agent-runtimes SOPS env, stored at ~/.claude-secrets/minimax-auth-token. Adding new providers = one YAML file, no script changes. 58 dryrun tests passing (up from 37). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,12 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# claude-profile — Launch Claude Code with a profile and engagement mode
|
||||
# claude-profile — Launch Claude Code with a profile, engagement mode, and provider
|
||||
#
|
||||
# Usage:
|
||||
# claude-profile # interactive: profile + mode
|
||||
# claude-profile <profile> # profile fixed, mode interactive
|
||||
# claude-profile --mode <name> # mode fixed, profile interactive
|
||||
# claude-profile --preset <name> # both from presets.yaml
|
||||
# claude-profile <profile> --mode <name> # both fixed
|
||||
# claude-profile # interactive: profile + mode + provider
|
||||
# claude-profile <profile> # profile fixed, rest interactive
|
||||
# claude-profile --mode <name> # mode fixed, profile + provider interactive
|
||||
# claude-profile --provider <name> # provider fixed, profile + mode interactive
|
||||
# claude-profile --preset <name> # all from presets.yaml
|
||||
# claude-profile <profile> --mode <name> # profile + mode fixed
|
||||
# claude-profile --dryrun [...] # show resolved values, do not launch
|
||||
# claude-profile [...] -- <claude-args> # extra args passed through to claude
|
||||
#
|
||||
@@ -22,6 +23,7 @@ SCRIPT_PATH="$(readlink -f "$0")"
|
||||
SCRIPT_DIR="$(dirname "$SCRIPT_PATH")"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
||||
MODES_DIR="$REPO_ROOT/data/claude-profile/modes"
|
||||
PROVIDERS_DIR="$REPO_ROOT/data/claude-profile/providers"
|
||||
|
||||
# context-load is on PATH (symlinked into ~/sbin from claude-foundations).
|
||||
# Fall back to a sibling location for testing isolation.
|
||||
@@ -31,18 +33,22 @@ else
|
||||
CONTEXT_LOADER="${SCRIPT_DIR}/context-load"
|
||||
fi
|
||||
|
||||
# Driver logical name -> full Claude model ID
|
||||
declare -A DRIVER_MAP=(
|
||||
[haiku]="claude-haiku-4-5-20251001"
|
||||
[sonnet]="claude-sonnet-4-6"
|
||||
[opus]="claude-opus-4-6"
|
||||
[opus-1m]="claude-opus-4-6[1m]"
|
||||
)
|
||||
# Driver logical name -> default provider (fallback when --provider not given)
|
||||
driver_default_provider() {
|
||||
case "$1" in
|
||||
haiku) echo "anthropic-haiku" ;;
|
||||
sonnet) echo "anthropic-sonnet" ;;
|
||||
opus) echo "anthropic-opus" ;;
|
||||
opus-1m) echo "anthropic-opus-1m" ;;
|
||||
*) err "Unknown driver '$1'. Valid: haiku sonnet opus opus-1m" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# === CLI parsing ===
|
||||
|
||||
PROFILE=""
|
||||
MODE=""
|
||||
PROVIDER=""
|
||||
PROJECT=""
|
||||
PRESET=""
|
||||
DRYRUN=0
|
||||
@@ -54,13 +60,14 @@ usage() {
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--mode) MODE="${2:-}"; shift 2 ;;
|
||||
--preset) PRESET="${2:-}"; shift 2 ;;
|
||||
--project) PROJECT="${2:-}"; shift 2 ;;
|
||||
--mode) MODE="${2:-}"; shift 2 ;;
|
||||
--provider) PROVIDER="${2:-}"; shift 2 ;;
|
||||
--preset) PRESET="${2:-}"; shift 2 ;;
|
||||
--project) PROJECT="${2:-}"; shift 2 ;;
|
||||
--dryrun|-n) DRYRUN=1; shift ;;
|
||||
--help|-h) usage; exit 0 ;;
|
||||
--) shift; CLAUDE_ARGS=("$@"); break ;;
|
||||
-*) echo "Unknown flag: $1" >&2; usage >&2; exit 1 ;;
|
||||
--help|-h) usage; exit 0 ;;
|
||||
--) shift; CLAUDE_ARGS=("$@"); break ;;
|
||||
-*) echo "Unknown flag: $1" >&2; usage >&2; exit 1 ;;
|
||||
*)
|
||||
if [[ -z "$PROFILE" ]]; then
|
||||
PROFILE="$1"
|
||||
@@ -74,8 +81,8 @@ while [[ $# -gt 0 ]]; do
|
||||
done
|
||||
|
||||
# Mutual exclusion
|
||||
if [[ -n "$PRESET" && ( -n "$MODE" || -n "$PROJECT" ) ]]; then
|
||||
echo "Error: --preset is mutually exclusive with --mode and --project" >&2
|
||||
if [[ -n "$PRESET" && ( -n "$MODE" || -n "$PROJECT" || -n "$PROVIDER" ) ]]; then
|
||||
echo "Error: --preset is mutually exclusive with --mode, --provider, and --project" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -138,6 +145,40 @@ read_preset_block() {
|
||||
' "$file"
|
||||
}
|
||||
|
||||
# Extract a scalar field from a provider YAML file (non-indented key: value).
|
||||
parse_provider_field() {
|
||||
local file=$1 key=$2
|
||||
awk -v key="$key" '
|
||||
/^extra_env:[[:space:]]*$/ { in_extra=1; next }
|
||||
/^[a-zA-Z_]/ { in_extra=0 }
|
||||
!in_extra {
|
||||
if (match($0, "^"key":[[:space:]]*")) {
|
||||
val = substr($0, RLENGTH+1)
|
||||
gsub(/^[[:space:]]*"?/, "", val)
|
||||
gsub(/"?[[:space:]]*$/, "", val)
|
||||
print val; exit
|
||||
}
|
||||
}
|
||||
' "$file"
|
||||
}
|
||||
|
||||
# Output KEY=value lines for the extra_env block in a provider YAML file.
|
||||
parse_extra_env() {
|
||||
local file=$1
|
||||
awk '
|
||||
/^extra_env:[[:space:]]*$/ { in_extra=1; next }
|
||||
/^[a-zA-Z_]/ { in_extra=0 }
|
||||
in_extra && /^[[:space:]]+[A-Z_]/ {
|
||||
line = $0; sub(/^[[:space:]]+/, "", line)
|
||||
key = line; val = line
|
||||
sub(/:.*$/, "", key)
|
||||
sub(/^[^:]+:[[:space:]]*/, "", val)
|
||||
gsub(/^[[:space:]]*"?/, "", val); gsub(/"?[[:space:]]*$/, "", val)
|
||||
print key "=" val
|
||||
}
|
||||
' "$file"
|
||||
}
|
||||
|
||||
# List available modes (sorted), printing a numbered menu.
|
||||
# Returns the array of mode names via the global MODE_LIST.
|
||||
MODE_LIST=()
|
||||
@@ -151,17 +192,25 @@ list_modes() {
|
||||
purpose=$(awk '/^## Purpose$/{getline; getline; print; exit}' "$f")
|
||||
printf " %d) %-7s — %s\n" "$i" "$name" "$purpose"
|
||||
MODE_LIST+=("$name")
|
||||
((i++))
|
||||
i=$((i + 1))
|
||||
done
|
||||
}
|
||||
|
||||
driver_to_model() {
|
||||
local d=$1
|
||||
local valid="${!DRIVER_MAP[*]}"
|
||||
if [[ -z "${DRIVER_MAP[$d]:-}" ]]; then
|
||||
err "Unknown driver '$d'. Valid: $valid"
|
||||
fi
|
||||
echo "${DRIVER_MAP[$d]}"
|
||||
# List available providers, printing a numbered menu.
|
||||
# Returns the array of provider names via the global PROVIDER_LIST.
|
||||
PROVIDER_LIST=()
|
||||
list_providers() {
|
||||
PROVIDER_LIST=()
|
||||
local i=1
|
||||
for f in "$PROVIDERS_DIR"/*.yaml; do
|
||||
[[ -f "$f" ]] || continue
|
||||
local name display
|
||||
name=$(parse_provider_field "$f" name)
|
||||
display=$(parse_provider_field "$f" display)
|
||||
printf " %d) %-22s — %s\n" "$i" "$name" "$display"
|
||||
PROVIDER_LIST+=("$name")
|
||||
i=$((i + 1))
|
||||
done
|
||||
}
|
||||
|
||||
resolve_profile_dir() {
|
||||
@@ -186,6 +235,7 @@ reset_wezterm_profile() {
|
||||
|
||||
# Sanity check on the modes directory before doing any picker work.
|
||||
[[ -d "$MODES_DIR" ]] || err "Mode files directory not found at $MODES_DIR"
|
||||
[[ -d "$PROVIDERS_DIR" ]] || err "Providers directory not found at $PROVIDERS_DIR"
|
||||
|
||||
if [[ -n "$PRESET" ]]; then
|
||||
# Find the preset across all profile presets.yaml files.
|
||||
@@ -203,12 +253,13 @@ if [[ -n "$PRESET" ]]; then
|
||||
done
|
||||
[[ -n "$preset_profile_dir" ]] || err "Preset '$PRESET' not found in any presets.yaml"
|
||||
|
||||
# Apply preset values (PRESET_PROFILE, PRESET_MODE, etc.)
|
||||
# Apply preset values
|
||||
while IFS='=' read -r k v; do
|
||||
[[ -z "$k" ]] && continue
|
||||
case "$k" in
|
||||
PRESET_PROFILE) preset_profile_name="$v" ;;
|
||||
PRESET_MODE) MODE="$v" ;;
|
||||
PRESET_PROVIDER) PROVIDER="$v" ;;
|
||||
PRESET_PROJECT) PROJECT="$v" ;;
|
||||
PRESET_TIME_HORIZON) TIME_HORIZON="$v" ;;
|
||||
PRESET_ASYNC) ASYNC_OVERRIDE="$v" ;;
|
||||
@@ -238,7 +289,7 @@ else
|
||||
name="${dir##*/.claude-}"
|
||||
echo " $i) $name (~/.claude-$name)"
|
||||
profiles+=("$name")
|
||||
((i++))
|
||||
i=$((i + 1))
|
||||
done
|
||||
echo ""
|
||||
if [[ -t 0 ]]; then
|
||||
@@ -329,14 +380,52 @@ fi
|
||||
[[ -n "${ASYNC_OVERRIDE:-}" ]] && ASYNC_OK="$ASYNC_OVERRIDE"
|
||||
[[ -n "${AUTOLOOP_OVERRIDE:-}" ]] && {
|
||||
if [[ "$AUTOLOOP_OVERRIDE" == "yes" || "$AUTOLOOP_OVERRIDE" == "no" ]]; then
|
||||
: # leave AUTOLOOP as the mode default unless explicitly disabled
|
||||
[[ "$AUTOLOOP_OVERRIDE" == "no" ]] && AUTOLOOP="none"
|
||||
fi
|
||||
}
|
||||
|
||||
# === Phase 3: Launch ===
|
||||
# === Phase 2.5: Provider selection ===
|
||||
|
||||
MODEL_ID=$(driver_to_model "$DRIVER")
|
||||
default_provider=$(driver_default_provider "$DRIVER")
|
||||
|
||||
if [[ -z "$PROVIDER" ]]; then
|
||||
if [[ -z "$PRESET" ]]; then
|
||||
echo ""
|
||||
echo "Provider (model + API):"
|
||||
list_providers
|
||||
echo ""
|
||||
if [[ -t 0 ]]; then
|
||||
read -rp "Choose provider [$default_provider]: " prov_choice
|
||||
else
|
||||
prov_choice=""
|
||||
fi
|
||||
prov_choice="${prov_choice:-$default_provider}"
|
||||
|
||||
if [[ "$prov_choice" =~ ^[0-9]+$ ]]; then
|
||||
if (( prov_choice < 1 || prov_choice > ${#PROVIDER_LIST[@]} )); then
|
||||
err "Invalid provider choice: $prov_choice"
|
||||
fi
|
||||
PROVIDER="${PROVIDER_LIST[$((prov_choice - 1))]}"
|
||||
else
|
||||
PROVIDER="$prov_choice"
|
||||
fi
|
||||
else
|
||||
PROVIDER="$default_provider"
|
||||
fi
|
||||
fi
|
||||
|
||||
provider_file="$PROVIDERS_DIR/$PROVIDER.yaml"
|
||||
[[ -f "$provider_file" ]] || err "Provider not found: $PROVIDER (looked in $PROVIDERS_DIR)"
|
||||
|
||||
PROVIDER_DISPLAY=$(parse_provider_field "$provider_file" display)
|
||||
MODEL_ID=$(parse_provider_field "$provider_file" model_id)
|
||||
PROVIDER_BASE_URL=$(parse_provider_field "$provider_file" base_url)
|
||||
PROVIDER_API_KEY_ENV=$(parse_provider_field "$provider_file" api_key_env)
|
||||
PROVIDER_API_KEY_FILE=$(parse_provider_field "$provider_file" api_key_file)
|
||||
|
||||
[[ -n "$MODEL_ID" ]] || err "Provider file $provider_file is missing required field: model_id"
|
||||
|
||||
# === Phase 3: Launch ===
|
||||
|
||||
# Validate context-load exists
|
||||
[[ -x "$CONTEXT_LOADER" ]] || err "context-load not found at $CONTEXT_LOADER"
|
||||
@@ -346,6 +435,7 @@ active_env=$(cat <<EOF
|
||||
CLAUDE_MODE=$MODE
|
||||
CLAUDE_MODE_TAG=$TAG
|
||||
CLAUDE_DRIVER=$DRIVER
|
||||
CLAUDE_PROVIDER=$PROVIDER
|
||||
CLAUDE_ESCALATES_TO=${ESCALATES_TO:-none}
|
||||
CLAUDE_PROJECT=${PROJECT:-}
|
||||
CLAUDE_TIME_HORIZON=${TIME_HORIZON:-}
|
||||
@@ -374,11 +464,14 @@ combined_context="${context}
|
||||
${body}"
|
||||
|
||||
if (( DRYRUN )); then
|
||||
dryrun_log "Profile: $PROFILE ($target)"
|
||||
dryrun_log "Profile: $PROFILE ($target)"
|
||||
dryrun_log "Mode: $MODE"
|
||||
dryrun_log "Mode file: $mode_file"
|
||||
dryrun_log "Driver: $DRIVER"
|
||||
dryrun_log "Driver model: $MODEL_ID"
|
||||
dryrun_log "Provider: $PROVIDER ($PROVIDER_DISPLAY)"
|
||||
dryrun_log "Model ID: $MODEL_ID"
|
||||
[[ -n "$PROVIDER_BASE_URL" ]] && dryrun_log "Base URL: $PROVIDER_BASE_URL"
|
||||
[[ -n "$PROVIDER_API_KEY_ENV" ]] && dryrun_log "API key env: $PROVIDER_API_KEY_ENV (from $PROVIDER_API_KEY_FILE)"
|
||||
dryrun_log "Project: ${PROJECT:-<not set, will be picked by CLAUDE.md>}"
|
||||
dryrun_log "Time horizon: ${TIME_HORIZON:-<not asked in this mode>}"
|
||||
dryrun_log "Async OK: $ASYNC_OK"
|
||||
@@ -408,9 +501,24 @@ set_wezterm_profile "$PROFILE"
|
||||
# Disable adaptive thinking (existing behaviour)
|
||||
export CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1
|
||||
|
||||
# Export provider env vars for non-Anthropic providers
|
||||
if [[ -n "$PROVIDER_BASE_URL" ]]; then
|
||||
export ANTHROPIC_BASE_URL="$PROVIDER_BASE_URL"
|
||||
fi
|
||||
if [[ -n "$PROVIDER_API_KEY_ENV" && -n "$PROVIDER_API_KEY_FILE" ]]; then
|
||||
expanded_key_file="${PROVIDER_API_KEY_FILE/#\~/$HOME}"
|
||||
[[ -f "$expanded_key_file" ]] || err "Provider API key file not found: $expanded_key_file"
|
||||
api_key=$(cat "$expanded_key_file")
|
||||
export "${PROVIDER_API_KEY_ENV}=${api_key}"
|
||||
fi
|
||||
# Export extra_env entries from provider file
|
||||
while IFS='=' read -r k v; do
|
||||
[[ -n "$k" ]] && export "${k}=${v}"
|
||||
done < <(parse_extra_env "$provider_file")
|
||||
|
||||
# Write state files
|
||||
printf '%s\n' "$active_env" > "$target/active-mode.env"
|
||||
printf '%s\n' "$MODE" > "$target/last-mode"
|
||||
|
||||
echo "Launching: $PROFILE / $MODE${PROJECT:+ / $PROJECT}"
|
||||
echo "Launching: $PROFILE / $MODE / $PROVIDER${PROJECT:+ / $PROJECT}"
|
||||
exec claude --model "$MODEL_ID" --append-system-prompt "$combined_context" "${CLAUDE_ARGS[@]}"
|
||||
|
||||
Reference in New Issue
Block a user