From 111b857cdad18cd5222cb1a0d191b74802815062 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Thu, 27 Aug 2026 15:55:15 -0400 Subject: [PATCH] Add local Ollama as an AI Hub provider for claude-code Ollama v0.14+ speaks the Anthropic Messages API natively, so the registry drives it with a fixed 'ollama' token and runtime-listed models: static_token skips secret storage everywhere keys resolve, models_dynamic defers model validation to the live /api/tags list, and the setup wizard installs the right GPU variant and starts the service before listing local models. Doctor stops treating the endpoint port as part of the hostname. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_019hLK3wsDuKVAC37GgDqg6H --- bin/omarchy-cn-ai-default | 5 +++-- bin/omarchy-cn-ai-doctor | 4 +++- bin/omarchy-cn-ai-launch | 2 +- bin/omarchy-cn-ai-profile-create | 6 ++++-- bin/omarchy-cn-ai-setup | 28 ++++++++++++++++++++++++++-- bin/omarchy-cn-ai-test | 2 +- cn/lib/ai.sh | 18 +++++++++++++++++- cn/registry/ai-compatibility.json | 5 +++++ cn/registry/ai-providers.json | 14 +++++++++++++- 9 files changed, 73 insertions(+), 11 deletions(-) diff --git a/bin/omarchy-cn-ai-default b/bin/omarchy-cn-ai-default index c2927ab8..6f58ca3a 100755 --- a/bin/omarchy-cn-ai-default +++ b/bin/omarchy-cn-ai-default @@ -36,7 +36,7 @@ if omarchy-cmd-missing "$command"; then $install fi -key=$(omarchy-cn-ai-secret get "$provider") +key=$(cn_ai_resolve_key "$provider") case "$harness" in claude-code) @@ -45,7 +45,8 @@ claude-code) settings="$HOME/.claude/settings.json" mkdir -p "${settings%/*}" base=$(cn_ai_endpoint "$provider" anthropic) - fast=$(cn_ai_fast_model "$provider") + # Dynamic-model providers have no fast alias: every tier maps to the model + fast=$(cn_ai_fast_model "$provider" 2>/dev/null || printf '%s' "$model") current="{}" if [[ -s $settings ]]; then current=$(cat "$settings") diff --git a/bin/omarchy-cn-ai-doctor b/bin/omarchy-cn-ai-doctor index d8533a83..1b7f1d01 100755 --- a/bin/omarchy-cn-ai-doctor +++ b/bin/omarchy-cn-ai-doctor @@ -38,6 +38,8 @@ fi if [[ $proto == "native" ]]; then echo "INFO ai: $harness 自管鉴权,跳过 OmarchyCN Key 检查" +elif cn_ai_static_token "$provider" > /dev/null 2>&1; then + echo "PASS ai: $provider 使用内置 token,无需存储 Key" elif omarchy-cn-ai-secret get "$provider" > /dev/null 2>&1; then echo "PASS ai: $provider API Key stored" else @@ -47,7 +49,7 @@ fi if [[ $proto != "native" ]]; then base=$(cn_ai_endpoint "$provider" "$proto") - host=$(sed -E 's|https?://([^/]+).*|\1|' <<<"$base") + host=$(sed -E 's|https?://([^/:]+).*|\1|' <<<"$base") if getent hosts "$host" > /dev/null 2>&1; then echo "PASS ai: endpoint DNS resolves ($host)" else diff --git a/bin/omarchy-cn-ai-launch b/bin/omarchy-cn-ai-launch index 4ce827b4..c4800d22 100755 --- a/bin/omarchy-cn-ai-launch +++ b/bin/omarchy-cn-ai-launch @@ -32,7 +32,7 @@ fi case "$config_method" in env) - key=$(omarchy-cn-ai-secret get "$provider") + key=$(cn_ai_resolve_key "$provider") if [[ $harness == "codex" ]]; then cn_ai_render_codex_config "$provider" "$model" "$key" exec "$command" diff --git a/bin/omarchy-cn-ai-profile-create b/bin/omarchy-cn-ai-profile-create index e05b3f4c..ca291482 100755 --- a/bin/omarchy-cn-ai-profile-create +++ b/bin/omarchy-cn-ai-profile-create @@ -33,7 +33,9 @@ if [[ $level == "unsupported" ]]; then fi allowlist=$(cn_ai_combo_models "$harness" "$provider") -if [[ -n $allowlist ]]; then +if cn_ai_models_dynamic "$provider"; then + : # runtime-listed models; omarchycn ai test validates against the live server +elif [[ -n $allowlist ]]; then # Combo-restricted model set (e.g. codex uses its own catalog slugs) if ! grep -qxF "$model" <<<"$allowlist"; then echo "Model $model not supported for $harness x $provider" >&2 @@ -68,6 +70,6 @@ secret_ref = "omarchycn://ai/$provider" EOF echo "Profile $name: $harness x $provider x $model (compat: $level)" -if ! omarchy-cn-ai-secret get "$provider" > /dev/null 2>&1; then +if ! cn_ai_resolve_key "$provider" > /dev/null 2>&1; then echo "提示: 尚未存储 $provider 的 API Key,运行: omarchycn ai secret set $provider" fi diff --git a/bin/omarchy-cn-ai-setup b/bin/omarchy-cn-ai-setup index 7ed8833b..5255525a 100755 --- a/bin/omarchy-cn-ai-setup +++ b/bin/omarchy-cn-ai-setup @@ -24,7 +24,31 @@ provider=$(printf '%s\n' "${providers[@]}" | gum choose --header "选择 Provide harness_proto=$(cn_ai_harness_field "$harness" protocol) allowlist=$(cn_ai_combo_models "$harness" "$provider") -if [[ -n $allowlist ]]; then +if cn_ai_models_dynamic "$provider"; then + base=$(cn_ai_endpoint "$provider" "$harness_proto") + if [[ $provider == "ollama" ]]; then + if omarchy-cmd-missing ollama; then + gum confirm "Ollama 未安装,现在安装本地服务?" || exit 1 + if omarchy-cmd-present nvidia-smi; then + ollama_pkg=ollama-cuda + elif omarchy-cmd-present rocminfo; then + ollama_pkg=ollama-rocm + else + ollama_pkg=ollama + fi + omarchy-pkg-add "$ollama_pkg" + fi + if ! curl -sf --connect-timeout 3 "$base/api/tags" > /dev/null; then + sudo systemctl enable --now ollama + fi + fi + models=$(curl -sf --connect-timeout 3 "$base/api/tags" | jq -r '.models[].name' || true) + if [[ -z $models ]]; then + echo "无法列出本地模型(服务未运行或尚未拉取):先 ollama pull <模型>(如 qwen3-coder)再重试" >&2 + exit 1 + fi + model=$(gum choose --header "选择本地模型" <<<"$models") +elif [[ -n $allowlist ]]; then model=$(gum choose --header "选择模型" <<<"$allowlist") else # Hide models bound to a different wire protocol than the harness speaks @@ -48,7 +72,7 @@ if [[ $proto == "native" ]]; then exit 0 fi -if ! omarchy-cn-ai-secret get "$provider" > /dev/null 2>&1; then +if ! cn_ai_resolve_key "$provider" > /dev/null 2>&1; then key=$(gum input --password --header "$provider API Key(获取: $(jq -r --arg p "$provider" '.providers[$p].key_url' "$CN_AI_PROVIDERS"))") printf '%s' "$key" | omarchy-cn-ai-secret set "$provider" fi diff --git a/bin/omarchy-cn-ai-test b/bin/omarchy-cn-ai-test index db4eb20f..3ac1c544 100755 --- a/bin/omarchy-cn-ai-test +++ b/bin/omarchy-cn-ai-test @@ -31,7 +31,7 @@ fi base="${OMARCHYCN_AI_TEST_BASE_URL:-$(cn_ai_endpoint "$provider" "${proto%-responses}")}" echo "INFO ai-test: $profile ($harness x $provider x $model) -> $base" -if ! key=$(omarchy-cn-ai-secret get "$provider" 2>/dev/null); then +if ! key=$(cn_ai_resolve_key "$provider" 2>/dev/null); then echo "FAIL ai-test: 未存储 $provider 的 API Key (omarchycn ai secret set $provider)" exit 1 fi diff --git a/cn/lib/ai.sh b/cn/lib/ai.sh index 12928488..6f3a39d1 100644 --- a/cn/lib/ai.sh +++ b/cn/lib/ai.sh @@ -31,6 +31,21 @@ cn_ai_harness_field() { jq -re --arg h "$1" --arg f "$2" '.harnesses[$h][$f]' "$CN_AI_HARNESSES" } +# Providers with a fixed token (local servers) need no stored secret. +# `// empty` keeps stdout clean when absent (bare -re would print "null"). +cn_ai_static_token() { + jq -re --arg p "$1" '.providers[$p].static_token // empty' "$CN_AI_PROVIDERS" +} + +cn_ai_resolve_key() { + cn_ai_static_token "$1" 2>/dev/null || omarchy-cn-ai-secret get "$1" +} + +# Dynamic-model providers list models at runtime, not in the registry +cn_ai_models_dynamic() { + jq -e --arg p "$1" '.providers[$p].models_dynamic == true' "$CN_AI_PROVIDERS" > /dev/null +} + cn_ai_combo_level() { jq -r --arg h "$1" --arg p "$2" '.combos[$h][$p].level // "unsupported"' "$CN_AI_COMPAT" } @@ -68,7 +83,8 @@ cn_ai_render_env() { case "$harness" in claude-code) base=$(cn_ai_endpoint "$provider" anthropic) - fast=$(cn_ai_fast_model "$provider") + # Dynamic-model providers have no fast alias: every tier maps to the model + fast=$(cn_ai_fast_model "$provider" 2>/dev/null || printf '%s' "$model") printf 'export ANTHROPIC_BASE_URL=%q\n' "$base" # Docs vary between AUTH_TOKEN (deepseek/zai) and API_KEY (kimi): set both printf 'export ANTHROPIC_AUTH_TOKEN=%q\n' "$key" diff --git a/cn/registry/ai-compatibility.json b/cn/registry/ai-compatibility.json index 30482104..ce15ac3e 100644 --- a/cn/registry/ai-compatibility.json +++ b/cn/registry/ai-compatibility.json @@ -21,6 +21,11 @@ "level": "stable", "doc": "https://platform.minimax.io/docs/token-plan/claude-code", "adapter": true + }, + "ollama": { + "level": "stable", + "doc": "https://docs.ollama.com/api/anthropic-compatibility", + "adapter": true } }, "codex": { diff --git a/cn/registry/ai-providers.json b/cn/registry/ai-providers.json index b14dd112..a37cd102 100644 --- a/cn/registry/ai-providers.json +++ b/cn/registry/ai-providers.json @@ -1,5 +1,5 @@ { - "_verified": "2026-08-25, from official docs (see OmarchyCN PRD §27; minimax: platform.minimax.io/docs/token-plan/claude-code)", + "_verified": "2026-08-27, from official docs (see OmarchyCN PRD §27; minimax: platform.minimax.io/docs/token-plan/claude-code; ollama: docs.ollama.com/api/anthropic-compatibility, fixed token \"ollama\", models are whatever is pulled locally)", "providers": { "deepseek": { "display_name": "DeepSeek", @@ -124,6 +124,18 @@ } ] }, + "ollama": { + "display_name": "Ollama 本地", + "auth": [ + "none" + ], + "static_token": "ollama", + "endpoints": { + "anthropic": "http://localhost:11434" + }, + "models": [], + "models_dynamic": true + }, "minimax": { "display_name": "MiniMax", "auth": [