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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019hLK3wsDuKVAC37GgDqg6H
This commit is contained in:
2026-08-27 15:55:15 -04:00
co-authored by Claude Fable 5
parent 4b280a1767
commit 111b857cda
9 changed files with 73 additions and 11 deletions
+3 -2
View File
@@ -36,7 +36,7 @@ if omarchy-cmd-missing "$command"; then
$install $install
fi fi
key=$(omarchy-cn-ai-secret get "$provider") key=$(cn_ai_resolve_key "$provider")
case "$harness" in case "$harness" in
claude-code) claude-code)
@@ -45,7 +45,8 @@ claude-code)
settings="$HOME/.claude/settings.json" settings="$HOME/.claude/settings.json"
mkdir -p "${settings%/*}" mkdir -p "${settings%/*}"
base=$(cn_ai_endpoint "$provider" anthropic) 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="{}" current="{}"
if [[ -s $settings ]]; then if [[ -s $settings ]]; then
current=$(cat "$settings") current=$(cat "$settings")
+3 -1
View File
@@ -38,6 +38,8 @@ fi
if [[ $proto == "native" ]]; then if [[ $proto == "native" ]]; then
echo "INFO ai: $harness 自管鉴权,跳过 OmarchyCN Key 检查" 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 elif omarchy-cn-ai-secret get "$provider" > /dev/null 2>&1; then
echo "PASS ai: $provider API Key stored" echo "PASS ai: $provider API Key stored"
else else
@@ -47,7 +49,7 @@ fi
if [[ $proto != "native" ]]; then if [[ $proto != "native" ]]; then
base=$(cn_ai_endpoint "$provider" "$proto") 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 if getent hosts "$host" > /dev/null 2>&1; then
echo "PASS ai: endpoint DNS resolves ($host)" echo "PASS ai: endpoint DNS resolves ($host)"
else else
+1 -1
View File
@@ -32,7 +32,7 @@ fi
case "$config_method" in case "$config_method" in
env) env)
key=$(omarchy-cn-ai-secret get "$provider") key=$(cn_ai_resolve_key "$provider")
if [[ $harness == "codex" ]]; then if [[ $harness == "codex" ]]; then
cn_ai_render_codex_config "$provider" "$model" "$key" cn_ai_render_codex_config "$provider" "$model" "$key"
exec "$command" exec "$command"
+4 -2
View File
@@ -33,7 +33,9 @@ if [[ $level == "unsupported" ]]; then
fi fi
allowlist=$(cn_ai_combo_models "$harness" "$provider") 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) # Combo-restricted model set (e.g. codex uses its own catalog slugs)
if ! grep -qxF "$model" <<<"$allowlist"; then if ! grep -qxF "$model" <<<"$allowlist"; then
echo "Model $model not supported for $harness x $provider" >&2 echo "Model $model not supported for $harness x $provider" >&2
@@ -68,6 +70,6 @@ secret_ref = "omarchycn://ai/$provider"
EOF EOF
echo "Profile $name: $harness x $provider x $model (compat: $level)" 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" echo "提示: 尚未存储 $provider 的 API Key,运行: omarchycn ai secret set $provider"
fi fi
+26 -2
View File
@@ -24,7 +24,31 @@ provider=$(printf '%s\n' "${providers[@]}" | gum choose --header "选择 Provide
harness_proto=$(cn_ai_harness_field "$harness" protocol) harness_proto=$(cn_ai_harness_field "$harness" protocol)
allowlist=$(cn_ai_combo_models "$harness" "$provider") 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") model=$(gum choose --header "选择模型" <<<"$allowlist")
else else
# Hide models bound to a different wire protocol than the harness speaks # Hide models bound to a different wire protocol than the harness speaks
@@ -48,7 +72,7 @@ if [[ $proto == "native" ]]; then
exit 0 exit 0
fi 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")") 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" printf '%s' "$key" | omarchy-cn-ai-secret set "$provider"
fi fi
+1 -1
View File
@@ -31,7 +31,7 @@ fi
base="${OMARCHYCN_AI_TEST_BASE_URL:-$(cn_ai_endpoint "$provider" "${proto%-responses}")}" base="${OMARCHYCN_AI_TEST_BASE_URL:-$(cn_ai_endpoint "$provider" "${proto%-responses}")}"
echo "INFO ai-test: $profile ($harness x $provider x $model) -> $base" 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)" echo "FAIL ai-test: 未存储 $provider 的 API Key (omarchycn ai secret set $provider)"
exit 1 exit 1
fi fi
+17 -1
View File
@@ -31,6 +31,21 @@ cn_ai_harness_field() {
jq -re --arg h "$1" --arg f "$2" '.harnesses[$h][$f]' "$CN_AI_HARNESSES" 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() { cn_ai_combo_level() {
jq -r --arg h "$1" --arg p "$2" '.combos[$h][$p].level // "unsupported"' "$CN_AI_COMPAT" 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 case "$harness" in
claude-code) claude-code)
base=$(cn_ai_endpoint "$provider" anthropic) 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" printf 'export ANTHROPIC_BASE_URL=%q\n' "$base"
# Docs vary between AUTH_TOKEN (deepseek/zai) and API_KEY (kimi): set both # Docs vary between AUTH_TOKEN (deepseek/zai) and API_KEY (kimi): set both
printf 'export ANTHROPIC_AUTH_TOKEN=%q\n' "$key" printf 'export ANTHROPIC_AUTH_TOKEN=%q\n' "$key"
+5
View File
@@ -21,6 +21,11 @@
"level": "stable", "level": "stable",
"doc": "https://platform.minimax.io/docs/token-plan/claude-code", "doc": "https://platform.minimax.io/docs/token-plan/claude-code",
"adapter": true "adapter": true
},
"ollama": {
"level": "stable",
"doc": "https://docs.ollama.com/api/anthropic-compatibility",
"adapter": true
} }
}, },
"codex": { "codex": {
+13 -1
View File
@@ -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": { "providers": {
"deepseek": { "deepseek": {
"display_name": "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": { "minimax": {
"display_name": "MiniMax", "display_name": "MiniMax",
"auth": [ "auth": [