#!/usr/bin/env bash # qwclaude-desktop — configure Claude Desktop to use Alibaba Cloud Bailian (Qwen) as inference backend. # # Edits ~/Library/Application Support/Claude-3p/configLibrary/{_meta,}.json # and restarts Claude Desktop. macOS only. # # Usage: # qwclaude-desktop # pay-as-you-go, qwen3.7-max (Beijing), then restart # qwclaude-desktop plus # pay-as-you-go, qwen3.7-plus (balanced, cheaper) # qwclaude-desktop intl # pay-as-you-go on the Singapore endpoint # qwclaude-desktop coding # Coding Plan (qwen3.7-plus) # qwclaude-desktop token # Token Plan team edition (qwen3.7-max) # qwclaude-desktop token plus # Token Plan, qwen3.7-plus # qwclaude-desktop update # git pull latest from the repo # qwclaude-desktop -h # help # # Reads the plan-specific Bailian API key from env or ~/.zshrc & friends (else # prompts): pay-as-you-go → DASHSCOPE_API_KEY, Coding Plan → DASHSCOPE_CP_API_KEY, # Token Plan → DASHSCOPE_TP_API_KEY. # # Note: Once a third-party gateway is active, Claude Desktop's Chat mode is # unavailable — only Cowork (3P) and Code modes work (Chat depends on # Anthropic-hosted features). To use Chat: at Claude Desktop's launch chooser # pick "Continue with Anthropic", or toggle "Skip login-mode chooser" off in # Developer → Configure Third-Party Inference. set -euo pipefail CONFIG_DIR="$HOME/Library/Application Support/Claude-3p/configLibrary" META="$CONFIG_DIR/_meta.json" ENTRY_NAME="qwclaude-desktop" AUTH_SCHEME="bearer" PLAN="payg" REGION="cn" PLUS_MODE=0 while [[ $# -gt 0 ]]; do case "$1" in -h|--help) sed -n '2,26p' "$0" | sed 's/^# \{0,1\}//' exit 0 ;; update) SELF_DIR="$(cd "$(dirname "$0")" && pwd)" echo "qwclaude-desktop: pulling latest from $SELF_DIR ..." git -C "$SELF_DIR" pull && echo "qwclaude-desktop: updated." || { echo "qwclaude-desktop: git pull failed. Check network or resolve conflicts manually." >&2 exit 1 } exit 0 ;; coding) PLAN="coding"; shift ;; token-plan|token|tp) PLAN="token-plan"; shift ;; payg|pay-as-you-go) PLAN="payg"; shift ;; intl|singapore|sg) REGION="intl"; shift ;; cn|beijing) REGION="cn"; shift ;; plus) PLUS_MODE=1; shift ;; max) PLUS_MODE=0; shift ;; *) echo "qwclaude-desktop: unknown argument: $1" >&2 echo "Run 'qwclaude-desktop -h' for usage." >&2 exit 2 ;; esac done # Resolve base URL, models, key variable, and label from the chosen plan. case "$PLAN" in token-plan) BASE_URL="https://token-plan.cn-beijing.maas.aliyuncs.com/apps/anthropic" if [ "$PLUS_MODE" = "1" ]; then MAIN_MODEL="qwen3.7-plus" else MAIN_MODEL="qwen3.7-max" fi FAST_MODEL="qwen3.6-flash" KEY_VAR="DASHSCOPE_TP_API_KEY" PLAN_LABEL="Token Plan" ;; coding) # Coding Plan serves qwen3.7-plus as the recommended model. BASE_URL="https://coding.dashscope.aliyuncs.com/apps/anthropic" MAIN_MODEL="qwen3.7-plus" FAST_MODEL="qwen3.6-plus" KEY_VAR="DASHSCOPE_CP_API_KEY" PLAN_LABEL="Coding Plan" ;; *) PLAN="payg" if [[ "$REGION" == "intl" ]]; then BASE_URL="https://dashscope-intl.aliyuncs.com/apps/anthropic" else BASE_URL="https://dashscope.aliyuncs.com/apps/anthropic" fi if [ "$PLUS_MODE" = "1" ]; then MAIN_MODEL="qwen3.7-plus" else MAIN_MODEL="qwen3.7-max" fi FAST_MODEL="qwen3.6-flash" KEY_VAR="DASHSCOPE_API_KEY" PLAN_LABEL="pay-as-you-go" ;; esac preflight() { if [[ "$(uname)" != "Darwin" ]]; then echo "qwclaude-desktop: macOS only." >&2 exit 1 fi if [[ ! -d "/Applications/Claude.app" ]]; then echo "qwclaude-desktop: /Applications/Claude.app not found. Install from https://claude.ai/download." >&2 exit 1 fi if [[ ! -x "/usr/bin/jq" ]]; then echo "qwclaude-desktop: /usr/bin/jq not found (required on macOS Sonoma+)." >&2 exit 1 fi # Claude Desktop's third-party inference is gated behind Developer Mode. local dev_settings="$HOME/Library/Application Support/Claude/developer_settings.json" if [[ ! -f "$dev_settings" ]] \ || ! /usr/bin/jq -e '.allowDevTools == true' "$dev_settings" >/dev/null 2>&1; then cat >&2 <<'EOF' qwclaude-desktop: Developer Mode not enabled in Claude Desktop. Claude Desktop's third-party inference feature is gated behind Developer Mode. Enable it once in the GUI before running this script: 1. Open Claude Desktop 2. Help → Troubleshooting → Enable Developer Mode 3. Re-run qwclaude-desktop EOF exit 1 fi } # Resolves the plan-specific key var from (in order): env, ~/.zshrc & friends, # osascript prompt. Prints the key on stdout. resolve_api_key() { local var="$1" if [[ -n "${!var:-}" ]]; then printf '%s' "${!var}" return 0 fi local rc found="" for rc in "$HOME/.zshrc" "$HOME/.bashrc" "$HOME/.bash_profile" "$HOME/.profile"; do [[ -r "$rc" ]] || continue found="$(grep -v '^[[:space:]]*#' "$rc" 2>/dev/null \ | grep -E "^[[:space:]]*export[[:space:]]+${var}=" \ | tail -1 \ | sed -E 's/^[^=]*=//; s/^"(.*)"$/\1/; s/^'\''(.*)'\''$/\1/' \ || true)" if [[ -n "$found" ]]; then printf '%s' "$found" return 0 fi done local prompt_msg="Bailian ${PLAN_LABEL} API key (${var}) not found in env or shell rc.\\nPaste your key:" local key key="$(osascript </dev/null || true try set theKey to text returned of (display dialog ¬ "${prompt_msg}" ¬ default answer "" ¬ with hidden answer ¬ with title "qwclaude-desktop") return theKey on error return "" end try APPLESCRIPT )" if [[ -z "$key" ]]; then echo "qwclaude-desktop: no $var provided. Aborting." >&2 exit 1 fi printf '%s' "$key" } confirm_or_abort() { echo echo "About to: $1" echo "Press Enter to continue, Ctrl-C to abort." read -r _ } # Write the gateway entry JSON for a given UUID via atomic temp+mv. write_entry() { local uuid="$1" local entry_path="$CONFIG_DIR/${uuid}.json" local tmp="${entry_path}.tmp" local content # unstableDisableModelVerification skips Claude Desktop 1.7xxx's local model-name # validator. Without it, names matching its hard-coded block-list # (deepseek/qwen/gemini/... and other non-Anthropic vendors) are rejected before # any request leaves the app. # Coding Plan has a single model, so the flash entry is dropped when identical. content="$(jq -n \ --arg baseUrl "$BASE_URL" \ --arg apiKey "$QWEN_KEY" \ --arg auth "$AUTH_SCHEME" \ --arg main "$MAIN_MODEL" \ --arg fast "$FAST_MODEL" \ '{ inferenceProvider: "gateway", inferenceGatewayBaseUrl: $baseUrl, inferenceGatewayApiKey: $apiKey, inferenceGatewayAuthScheme: $auth, unstableDisableModelVerification: true, inferenceModels: ([{name: $main, supports1m: false}] + (if $fast != $main then [{name: $fast, supports1m: false}] else [] end)) }')" printf '%s' "$content" > "$tmp" chmod 600 "$tmp" mv "$tmp" "$entry_path" } # Ensure _meta.json has an entry named $ENTRY_NAME (creating or reusing its uuid) # and set appliedId to that uuid. Prints the uuid on stdout. ensure_meta_entry() { mkdir -p "$CONFIG_DIR" local existing_uuid="" if [[ -f "$META" ]]; then existing_uuid="$(jq -r --arg name "$ENTRY_NAME" \ '.entries[]? | select(.name==$name) | .id' "$META" 2>/dev/null \ | head -1)" fi local uuid if [[ -n "$existing_uuid" ]]; then uuid="$existing_uuid" else uuid="$(uuidgen | tr 'A-Z' 'a-z')" fi local tmp="${META}.tmp" local content if [[ -f "$META" ]]; then content="$(jq --arg id "$uuid" --arg name "$ENTRY_NAME" ' .appliedId = $id | .entries = ((.entries // []) | map(select(.name != $name)) + [{id: $id, name: $name}]) ' "$META")" else content="$(jq -n --arg id "$uuid" --arg name "$ENTRY_NAME" \ '{appliedId: $id, entries: [{id: $id, name: $name}]}')" fi printf '%s' "$content" > "$tmp" chmod 600 "$tmp" mv "$tmp" "$META" printf '%s' "$uuid" } restart_claude() { killall Claude 2>/dev/null || true sleep 1 open -a Claude } preflight QWEN_KEY="$(resolve_api_key "$KEY_VAR")" confirm_or_abort "configure Claude Desktop to use Bailian $PLAN_LABEL ($BASE_URL, $MAIN_MODEL) and restart it." UUID="$(ensure_meta_entry)" write_entry "$UUID" restart_claude cat <