#!/usr/bin/env bash # Updates lmstudio and/or ollama model listings in opencode.json # by querying their /v1/models endpoints. # # Usage: # ./update-models.sh # update both # ./update-models.sh lmstudio # update only lmstudio # ./update-models.sh ollama # update only ollama # # Requires: jq, curl set -uo pipefail CONFIG="${OPENCODE_CONFIG:-${HOME}/.config/opencode/opencode.json}" if [[ ! -f "$CONFIG" ]]; then echo "Error: config not found at $CONFIG" >&2 exit 1 fi if ! command -v jq &>/dev/null; then echo "Error: jq is required. Install with: brew install jq" >&2 exit 1 fi # Map provider name -> base URL (read from config) get_base_url() { jq -r ".provider.${1}.options.baseURL // empty" "$CONFIG" } # Map provider name -> display label (read from config) get_display_name() { jq -r ".provider.${1}.name // \"${1}\"" "$CONFIG" } # Fetch models from API endpoint and build the opencode models object. # Preserves any existing entries that have extra metadata (limits, modalities, variants) # beyond just a name — so your manual overrides aren't lost. build_models_json() { local provider="$1" local base_url="$2" local display_name="$3" local api_response api_response=$(curl -sf --connect-timeout 5 "${base_url}/models" 2>/dev/null) || { echo "Error: could not reach ${base_url}/models — is ${provider} running?" >&2 return 1 } # Get list of model IDs from API local model_ids model_ids=$(echo "$api_response" | jq -r '.data[].id' | sort) if [[ -z "$model_ids" ]]; then echo "Warning: no models returned from ${provider}" >&2 return 1 fi # Get existing models object (to preserve manual overrides) local existing existing=$(jq ".provider.${provider}.models // {}" "$CONFIG") # Build new models object local new_models="{}" while IFS= read -r model_id; do [[ -z "$model_id" ]] && continue # Check if this model has extra config beyond just "name" local existing_entry existing_entry=$(echo "$existing" | jq -r --arg id "$model_id" '.[$id] // empty') local has_extras="false" if [[ -n "$existing_entry" ]]; then local key_count key_count=$(echo "$existing_entry" | jq 'keys | length') if [[ "$key_count" -gt 1 ]]; then has_extras="true" fi fi if [[ "$has_extras" == "true" ]]; then # Preserve existing entry with overrides, just update the name new_models=$(echo "$new_models" | jq \ --arg id "$model_id" \ --argjson entry "$existing_entry" \ --arg name "${model_id} (${display_name})" \ '.[$id] = ($entry | .name = $name)') else # Simple entry with just a name new_models=$(echo "$new_models" | jq \ --arg id "$model_id" \ --arg name "${model_id} (${display_name})" \ '.[$id] = { "name": $name }') fi done <<< "$model_ids" echo "$new_models" } update_provider() { local provider="$1" # Check provider exists in config if ! jq -e ".provider.${provider}" "$CONFIG" &>/dev/null; then echo "Skipping ${provider}: not configured in opencode.json" >&2 return 0 fi local base_url base_url=$(get_base_url "$provider") if [[ -z "$base_url" ]]; then echo "Skipping ${provider}: no baseURL configured" >&2 return 0 fi local display_name display_name=$(get_display_name "$provider") echo "Updating ${provider} models from ${base_url}..." local new_models new_models=$(build_models_json "$provider" "$base_url" "$display_name") || return 1 local count count=$(echo "$new_models" | jq 'keys | length') # Update config in-place local tmp tmp=$(mktemp) jq --argjson models "$new_models" \ ".provider.${provider}.models = \$models" \ "$CONFIG" > "$tmp" && mv "$tmp" "$CONFIG" echo " ✓ ${provider}: ${count} models synced" } # --- Main --- targets=("${@:-lmstudio ollama}") errors=0 for target in $targets; do update_provider "$target" || ((errors++)) done if [[ "$errors" -gt 0 ]]; then echo "Done with ${errors} error(s). Run 'opencode models' to verify." exit 1 else echo "Done. Run 'opencode models' to verify." fi