Files
env-ansible/tasks/workstation/linux/software/ai/llm.yml
T

1314 lines
58 KiB
YAML

---
# 2026-10-05 Hyperling
# Install a local LLM for use offline.
# - https://zeroclaw.net/blog/install-zeroclaw
# - https://zeroclaw.net/blog/set-up-zeroclaw-bot-ollama-telegram
#
# Packages Details
# - https://archlinux.org/packages/extra/x86_64/zeroclaw/
# - https://archlinux.org/packages/extra/x86_64/ollama/
#
# Individual Guides
# - https://github.com/zeroclaw-labs/zeroclaw
# - https://wiki.archlinux.org/title/Ollama
#
# Model Explanations
# - https://webscraft.org/blog/ollama-na-8-gb-ram-yaki-modeli-pratsyuyut-u-2026?lang=en
#
# Models
# - https://ollama.com/search
#
# References
# - https://docs.zeroclaw.com/master/en/reference/config.html
# - https://docs.ollama.com
# - https://docs.ollama.com/faq#setting-environment-variables-on-linux
# - https://modelpiper.com/blog/ollama-environment-variables
# - https://qwenlm.github.io/qwen-code-docs/en/users/configuration/settings/#environment-variables--env-files
# - https://qwenlm.github.io/qwen-code-docs/en/users/qwen-serve/
# Currently only testing this on Arch based distros (pacman).
- name: Workstation | Linux | Software | LLMs
block:
# Facts - Start #
- name: Workstation | Linux | Software | LLMs | Facts | Provisioned
ansible.builtin.debug:
var: "{{ item }}"
loop:
- ollama_enabled
- manage_models
- hf_token
- zeroclaw_enabled
- open_webui_enabled
- open_webui_port
- open_webui_key
- codium_config
- qwen_code_enabled
- qwen_code_key
- open_terminal_enabled
- open_terminal_port
- open_terminal_key
- name: Workstation | Linux | Software | LLMs | Facts 01
ansible.builtin.set_fact:
ollama: ollama
zeroclaw: zeroclaw
zerocode: zerocode
qwen_code: qwen-code
dummy_service: 'cronie'
dummy_website: 'https://hyperling.com'
# Good to keep low to prevent swap usage. Going to have a minimum of 3
# though, one for Cline, two for Continue (Agent + Autocomplete).
# 20261014: Going down to a static of 3 since we're focusing on dense.
llm_concurrency: 3 #"{{ [(ansible_memtotal_mb / 32000) | round(0, method='ceil'), 3] | max | int }}"
# Allow having multiple chats open without them just waiting each other.
# Also setting this to a minimum of 3 for using Codium with extensions.
# 20261014: Going down to 1 since only using CPUs, no GPUs. Otherwise
# the models seem to take excessively long to answer even small things.
#llm_parallel: 1 #"{{ [(ansible_processor_vcpus / 2) | round(0, method='ceil'), 3] | max | int }}"
# 20261027 Increased to 3 as a maximum.
# After more research, this is a more memory-based parameter. Basically
# the model allocates this number of KV caches, that way when it is
# running in multiple chats it does not need to drop context and reload.
# Still only uses X threads for all chats, but does not waste much time.
# So if set to 3, and doing 64K context, it really allocates 192k in RAM
# and supposedly if only doing 1 chat the model can use it all.
llm_parallel: "{{ [(ansible_memtotal_mb / 8000) | round(0, method='floor'), 3] | min | int }}"
# Most LLMs take a coefficient of about 1.2 to 1.5 of their file size in
# RAM, which is usually less than guessing a usage of weight*1000.
# Example:
# - qwen3.5:9b would be 6.6GB * 1.3 = 8580, rather than 9000.
# Conversely:
# - qwen3.5:4b would be 3.3GB * 1.3 = 4290, rather than 4000.
llm_size_coef: 1.3
# Base the context on MB of RAM, with a minumum 64k for decent thinking.
llm_context: "{{ [(ansible_memtotal_mb * 4) | round(-3, method='ceil'), 64000] | max | int }}"
llm_docker_network: open-webui-net
- name: Workstation | Linux | Software | LLMs | Facts 02
ansible.builtin.set_fact:
ollama_runit_dir: "{{ runit_script_dir }}/{{ ollama }}"
ollama_runit_log_dest: "{{ global_log }}/{{ ollama }}"
zeroclaw_runit_dir: "{{ runit_script_dir }}/{{ zeroclaw }}"
zeroclaw_runit_log_dest: "{{ global_log }}/{{ zeroclaw }}"
# Amount of output tokens allowed per response. Half of context is ok.
llm_tokens: "{{ (llm_context / 8) | int }}"
- name: Workstation | Linux | Software | LLMs | Facts 03
ansible.builtin.set_fact:
ollama_runit_script: "{{ ollama_runit_dir }}/{{ runit_service_script }}"
ollama_runit_log_dir: "{{ ollama_runit_dir }}/{{ runit_log_dir }}"
ollama_runit_log_config: "{{ ollama_runit_log_dest }}/{{ global_log_config }}"
zeroclaw_runit_script: "{{ zeroclaw_runit_dir }}/{{ runit_service_script }}"
zeroclaw_runit_log_dir: "{{ zeroclaw_runit_dir }}/{{ runit_log_dir }}"
zeroclaw_runit_log_config: "{{ zeroclaw_runit_log_dest }}/{{ global_log_config }}"
- name: Workstation | Linux | Software | LLMs | Facts 04
ansible.builtin.set_fact:
ollama_runit_log_script: "{{ ollama_runit_log_dir }}/{{ runit_log_script }}"
zeroclaw_runit_log_script: "{{ zeroclaw_runit_log_dir }}/{{ runit_log_script }}"
- name: Workstation | Linux | Software | LLMs | Facts | LLMs
ansible.builtin.set_fact:
llm_installs: []
llm_removals:
##### QWEN #####
- { "purpose": ["chat"], "model": "qwen3:4b", "provider": "qwen_30_0040", "size_mb": 4000, "context": 123456 } # succeeded
##### Mistral #####
- { "purpose": ["code"], "model": "devstral:24b", "provider": "mistral_dev10_0240", "size_mb": "{{ llm_size_coef * 14000 }}", "context": 128000 } # succeeded
# https://ollama.com/library/mistral-small : Last Updated 2025-01-30, succeeded
- { "purpose": ["chat"], "model": "mistral-small:24b", "provider": "mistral_small30_0240", "size_mb": "{{ llm_size_coef * 14000 }}", "context": 32000 }
- { "purpose": ["chat"], "model": "mistral-small:22b", "provider": "mistral_small30_0220", "size_mb": "{{ llm_size_coef * 13000 }}", "context": 128000 } # Yes, the smaller one has larger context.
# https://ollama.com/library/mistral-small3.1 : Last Update 2025-04-07, succeeded
- { "purpose": ["chat"], "model": "mistral-small3.1:24b", "provider": "mistral_small31_0240", "size_mb": "{{ llm_size_coef * 15000 }}", "context": 128000 }
llm_choices:
##### QWEN #####
### QWEN >3.5 - Need to try!
# Supposedly great for coding over any previous version.
# Prefer 27b (dense) over anything higher which likely uses MoE (essentially only uses 3b inside of a 35b, dumber but faster and potentially has access to more info).
# https://ollama.com/library/qwen3.8 : Last Updated 2026-09-25
# Known to overthink, suggested to prefer 3.6:27b unless setting temp to low/medium.
- { "purpose": ["code", "chat", "lang"], "model": "qwen3.8:27b", "provider": "qwen_38_0270", "size_mb": "{{ llm_size_coef * 18000 }}", "context": 262144 }
# https://ollama.com/library/qwen3.6 : Last Updated 2026-10-13 (yesterday as of this commit!!)
# Has been succeeded but dense and does not overthink so still worthy
- { "purpose": ["code", "chat", "lang"], "model": "qwen3.6:27b", "provider": "qwen_36_0270", "size_mb": "{{ llm_size_coef * 18000 }}", "context": 262144 }
- { "purpose": ["chat", "code", "lang"], "model": "qwen3.6:35b", "provider": "qwen_36_0350_moe", "size_mb": "{{ llm_size_coef * 23000 }}", "context": 262144 } # succeeded, not dense so not recommended
# https://ollama.com/library/qwen3.5 : Last Updated 2026-10-13 (yesterday as of this commit!!)
### QWEN 3.5 - Good for generic usage and a little coding.
- { "purpose": ["chat"], "model": "qwen3.5:0.8b", "provider": "qwen_35_0008", "size_mb": "{{ llm_size_coef * 1200 }}", "context": 262144 }
- { "purpose": ["chat"], "model": "qwen3.5:2b", "provider": "qwen_35_0020", "size_mb": "{{ llm_size_coef * 2700 }}", "context": 262144 }
- { "purpose": ["chat"], "model": "qwen3.5:4b", "provider": "qwen_35_0040", "size_mb": "{{ llm_size_coef * 3300 }}", "context": 262144 }
- { "purpose": ["chat"], "model": "qwen3.5:9b", "provider": "qwen_35_0090", "size_mb": "{{ llm_size_coef * 6600 }}", "context": 262144 }
- { "purpose": ["chat", "code"], "model": "qwen3.5:27b", "provider": "qwen_35_0270", "size_mb": "{{ llm_size_coef * 17000 }}", "context": 262144 } # succeeded
- { "purpose": ["chat", "code"], "model": "qwen3.5:35b", "provider": "qwen_35_0350_moe", "size_mb": "{{ llm_size_coef * 22000 }}", "context": 262144 }
- { "purpose": ["chat", "code"], "model": "qwen3.5:122b", "provider": "qwen_35_1220_moe", "size_mb": "{{ llm_size_coef * 81000 }}", "context": 262144 }
### QWEN 3.0 Coder - Supposed to be better than 2.5 for machines that can handle it.
- { "purpose": ["code", "chat"], "model": "qwen3-coder:30b", "provider": "qwen_30_coder_0300_moe", "size_mb": "{{ llm_size_coef * 19000 }}", "context": 262144 }
- { "purpose": ["code", "chat"], "model": "qwen3-coder:420b", "provider": "qwen_30_coder_4200_moe", "size_mb": "{{ llm_size_coef * 290000 }}", "context": 262144 }
### QWEN 2.5 Coder - Ol'reliable!
- { "purpose": ["code"], "model": "qwen2.5-coder:0.5b", "provider": "qwen_25_coder_0005", "size_mb": "{{ llm_size_coef * 398 }}", "context": 32768 }
- { "purpose": ["code"], "model": "qwen2.5-coder:1.5b", "provider": "qwen_25_coder_0015", "size_mb": "{{ llm_size_coef * 986 }}", "context": 32768 }
- { "purpose": ["code"], "model": "qwen2.5-coder:3b", "provider": "qwen_25_coder_0030", "size_mb": "{{ llm_size_coef * 1900 }}", "context": 32768 }
- { "purpose": ["code"], "model": "qwen2.5-coder:7b", "provider": "qwen_25_coder_0070", "size_mb": "{{ llm_size_coef * 4700 }}", "context": 32768 }
- { "purpose": ["code"], "model": "qwen2.5-coder:14b", "provider": "qwen_25_coder_0140", "size_mb": "{{ llm_size_coef * 9000 }}", "context": 32768 }
- { "purpose": ["code"], "model": "qwen2.5-coder:32b", "provider": "qwen_25_coder_0320", "size_mb": "{{ llm_size_coef * 20000 }}", "context": 32768 }
##### Mistral, Ministral, and Devstral - By a French company. #####
# mistral-small-4
# https://huggingface.co/unsloth/Mistral-Small-4-119B-2603-GGUF
# Supposed to be better than Qwen equivalents, it's basically mistral, mixtral, magistral, and devstral all in one.
# unsloth version is supposed to be the best quantization. UD-Q3_K_XL is supposed to take 64GB RAM, lol, while Q4_K_M would need 80+ GB.
# needs done via 'ollama create', fails with pull. https://github.com/ollama/ollama/issues/5245
- { "purpose": ["chat", "lang", "code"], "model": "hf.co/unsloth/Mistral-Small-4-119B-2603-GGUF:UD-Q3_K_XL", "provider": "mistral_small40_1190_unsloth_q3", "size_mb": "{{ llm_size_coef * 54800 }}", "context": 256000 }
# https://huggingface.co/mistralai/Mistral-Small-4-119B-2603 : 242GB unquantized, lol.
- { "purpose": ["chat", "lang", "code"], "model": "hf.co/mistralai/Mistral-Small-4-119B-2603", "provider": "mistral_small40_1190", "size_mb": "{{ llm_size_coef * 242000 }}", "context": 256000 }
# https://huggingface.co/unsloth/Mistral-Small-4-119B-2603 : 121GB, A6B MoE, hmmm!
- { "purpose": ["chat", "lang", "code"], "model": "hf.co/unsloth/Mistral-Small-4-119B-2603", "provider": "mistral_small40_1190_unsloth_a6b", "size_mb": "{{ llm_size_coef * 121000 }}", "context": 256000 }
# https://ollama.com/library/devstral-small-2 : Last Updated 2025-12-13
# Most recommended in Mistral family for agentic coding.
- { "purpose": ["code"], "model": "devstral-small-2:24b", "provider": "mistral_dev20_0240_small", "size_mb": "{{ llm_size_coef * 15000 }}", "context": 393216 }
# https://ollama.com/library/codestral : Last Updated 2024-09-03
# Mistral's first ever coder agent, recommended for autocomplete (FIM).
- { "purpose": ["code"], "model": "codestral:22b", "provider": "mistral_code_0220", "size_mb": "{{ llm_size_coef * 12000 }}", "context": 32768 }
# https://ollama.com/library/mistral : Last Updated 2025-07-13
- { "purpose": ["chat"], "model": "mistral:7b", "provider": "mistral_gen_0070", "size_mb": "{{ llm_size_coef * 4400 }}", "context": 32768 }
# https://ollama.com/library/mistral-small3.2 : Last Updated 2025-06-20
- { "purpose": ["chat", "lang"], "model": "mistral-small3.2:24b", "provider": "mistral_small32_0240", "size_mb": "{{ llm_size_coef * 15000 }}", "context": 128000 } # rated best for Croatian
# https://ollama.com/library/ministral-3 : Last Updated 2025-12-13
- { "purpose": ["chat", "lang"], "model": "ministral-3:3b", "provider": "mistral_mini30_0030", "size_mb": "{{ llm_size_coef * 3000 }}", "context": 256000 }
- { "purpose": ["chat", "lang"], "model": "ministral-3:8b", "provider": "mistral_mini30_0080", "size_mb": "{{ llm_size_coef * 6000 }}", "context": 256000 }
- { "purpose": ["chat", "lang"], "model": "ministral-3:14b", "provider": "mistral_mini30_0140", "size_mb": "{{ llm_size_coef * 9100 }}", "context": 256000 } # also rated super high for Croatian, gives more head room.
# https://ollama.com/library/mistral-nemo : Last Updated 2025-07-23
# Supposed to be a drop-in replacement for mistral:7b.
- { "purpose": ["chat", "lang"], "model": "mistral-nemo:12b", "provider": "mistral_nemo_0120", "size_mb": "{{ llm_size_coef * 7100 }}", "context": 128000 } # says 1000K in table but 128k in desc, going with desc.
##### TBD #####
### QWEN 2.5 - Older, but does not have "thinking", tempting!
#- qwen2.5:0.5b
#- qwen2.5:1.5b
#- qwen2.5:3b
#- qwen2.5:7b
#- qwen2.5:14b
#- qwen2.5:32b
#- qwen2.5:72b
### Z.ai
#- GLM-5.x
#- glm4-flash
### Others
#- deepseek-r1:1.5b
#- deepseek-r1:7b
- name: Workstation | Linux | Software | LLMs | Facts | Merge | Print
ansible.builtin.debug:
var: "{{ item }}"
loop:
- ansible_processor_vcpus
- ansible_memtotal_mb
- llm_concurrency
- llm_parallel
- llm_size_coef
- llm_context
- llm_tokens
- llm_installs
- llm_removals
- llm_choices
- name: Workstation | Linux | Software | LLMs | Facts | Choices | Sort
ansible.builtin.set_fact:
llm_installs: >-
{{ llm_choices | selectattr('size_mb', 'le', ansible_memtotal_mb) | list }}
llm_removals: >-
{{ llm_choices | selectattr('size_mb', 'gt', ansible_memtotal_mb) | list }}
- name: Workstation | Linux | Software | LLMs | Facts | Merge | Print
ansible.builtin.debug:
var: "{{ item }}"
loop:
- llm_installs
- llm_removals
# Facts - End #
- name: Workstation | Linux | Software | LLMs | Packages | Install
ansible.builtin.package:
name:
- "{{ ollama }}"
- "{{ zeroclaw }}"
- "{{ zerocode }}"
- "{{ qwen_code }}"
state: present
- name: Workstation | Linux | Software | LLMs | runit
block:
# Ollama - Start #
- name: Workstation | Linux | Software | LLMs | runit | ollama | Dir | Run
ansible.builtin.file:
name: "{{ ollama_runit_dir }}"
state: directory
owner: root
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | ollama | Dir | Log Run
ansible.builtin.file:
name: "{{ ollama_runit_log_dir }}"
state: directory
owner: root
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | ollama | Dir | Log Config
ansible.builtin.file:
name: "{{ ollama_runit_log_dest }}"
state: directory
owner: "{{ user }}"
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | ollama | Script | Run
ansible.builtin.copy:
dest: "{{ ollama_runit_script }}"
content: |
#!/bin/sh -e
exec 2>&1
# Allow pulling and accessing LLMs via tools.
### Required ###
export HOME={{ user_user.home }}
export OLLAMA_HOST=0.0.0.0 # Needed for Open WebUI
### Timeouts ###
export OLLAMA_LOAD_TIMEOUT=30m # default is 5m
export OLLAMA_REQUEST_TIMEOUT=3600 # default 300 (unconfirmed, not in the list of --help vars)
### Resource Usage ###
export OLLAMA_KEEP_ALIVE=-1 # default is 5m. Good frontends keep the model alive as long as it needs to be, this is not a source of timeout.
export OLLAMA_MAX_LOADED_MODELS={{ llm_concurrency }} # default is 3 if using CPUs, or 3 * NumGPUs
export OLLAMA_NUM_PARALLEL={{ llm_parallel }} # default is 1 and applies per-model
export OLLAMA_CONTEXT_LENGTH={{ llm_context }} # default is 4096, https://docs.ollama.com/faq#how-can-i-specify-the-context-window-size
export OLLAMA_MAX_QUEUE=999 # default is 512 (unconfirmed)
export OLLAMA_NUM_THREADS={{ (ansible_processor_vcpus / 2) | int }} # not real, but maybe some day.
### Prevent Data Loss ###
# OLLAMA_NOPRUNE
# Default is 0, which causes unfinished pulls to be deleted
# after 1 hour of inactivity. To manually clean partials, run:
# `rm ~/.ollama/models/blobs/*-partial*`
export OLLAMA_NOPRUNE=1
### Tokens ###
{{ ("export HF_TOKEN=" ~ hf_token) if hf_token else '' }}
### Logging ###
#export OLLAMA_DEBUG=1
export OLLAMA_METRICS=1 # (check http://127.0.0.1:11434/metrics)
### Other ###
export OLLAMA_ORIGINS=*
exec chpst -u {{ user }} ollama serve
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | ollama | Script | Finish
ansible.builtin.copy:
dest: /etc/sv/{{ ollama }}/finish
content: |
{{ runit_finish_sleep }}
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | ollama | Script | Log
ansible.builtin.copy:
dest: "{{ ollama_runit_log_script }}"
content: |
#!/bin/sh -e
exec svlogd -tt {{ ollama_runit_log_dest }}
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | ollama | Script | Log Config
ansible.builtin.copy:
dest: "{{ ollama_runit_log_config }}"
content: |
{{ runit_log_config_10mb }}
owner: "{{ user }}"
mode: "0755"
# Ollama - End #
# Zeroclaw - Start #
- name: Workstation | Linux | Software | LLMs | runit | zeroclaw | Dir | Run
ansible.builtin.file:
name: "{{ zeroclaw_runit_dir }}"
state: directory
owner: root
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | zeroclaw | Dir | Log
ansible.builtin.file:
name: "{{ zeroclaw_runit_log_dir }}"
state: directory
owner: root
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | zeroclaw | Dir | Log Config
ansible.builtin.file:
name: "{{ zeroclaw_runit_log_dest }}"
state: directory
owner: "{{ user }}"
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | zeroclaw | Script | File
ansible.builtin.copy:
dest: "{{ zeroclaw_runit_script }}"
content: |
#!/bin/sh -e
# Allow using the ZeroClaw frontend.
exec 2>&1
export HOME={{ user_user.home }}
export SHELL={{ bash_exec.stdout }}
exec chpst -u {{ user }} zeroclaw daemon -v --log-level info
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | zeroclaw | Script | Finish
ansible.builtin.copy:
dest: /etc/sv/{{ zeroclaw }}/finish
content: |
{{ runit_finish_sleep }}
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | zeroclaw | Script | Log
ansible.builtin.copy:
dest: "{{ zeroclaw_runit_log_script }}"
content: |
#!/bin/sh -e
exec svlogd -tt {{ zeroclaw_runit_log_dest }}
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | zeroclaw | Script | Log Config
ansible.builtin.copy:
dest: "{{ zeroclaw_runit_log_config }}"
content: |
{{ runit_log_config_10mb }}
mode: "0755"
# Zeroclaw - End #
# Open WebUI - Start #
- name: Workstation | Linux | Software | LLMs | runit | open-webui | Dir | /etc/sv
ansible.builtin.file:
name: "/etc/sv/open-webui/log"
state: directory
owner: root
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | open-webui | Dir | /var/log
ansible.builtin.file:
name: "/var/log/open-webui"
state: directory
owner: "{{ user }}"
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | open-webui | Script | Run
ansible.builtin.copy:
dest: /etc/sv/open-webui/run
content: |
#!/bin/sh
exec 2>&1
## TBD / TODO
## Move this to the same compose file as Open Terminal.
## Will solve quite a few network related complications.
## Also refactor the two into their own file (open_webui.yml).
## Probably also will then condense into 1 runit service.
image=ghcr.io/open-webui/open-webui:latest
# Wait for Docker to start up.
while [ ! -S /var/run/docker.sock ]; do sleep 1; done
# Ensure the old container is gone.
echo "RUN: Stopping..."
docker stop open-webui &&
echo "RUN: Container stopped." ||
echo "RUN: Container already stopped."
echo "RUN: Deleting..."
docker rm -f open-webui &&
echo "RUN: Container deleted." ||
echo "RUN: Container already deleted."
# Check for any new updates in the background.
(
while true; do
sleep 600
echo "RUN: Checking for updates..."
docker pull $image
done
) &
LOOP_PID=$!
# Ensure cleanup on any exit (normal or signal from runit).
trap 'kill "$LOOP_PID" 2>/dev/null; wait "$LOOP_PID" 2>/dev/null' EXIT
# Create a network for associated programs.
net={{ llm_docker_network }}
docker network inspect $net >/dev/null 2>&1 || \
docker network create $net
# Start the open-webui program.
set -e
docker run \
--name open-webui --network $net \
-e PORT={{ open_webui_port }} \
-p {{ open_webui_port }}:{{ open_webui_port }} \--add-host=host.docker.internal:host-gateway \
-e OLLAMA_BASE_URL=http://host.docker.internal:11434 \
{{ ("-e HF_TOKEN=" ~ hf_token) if hf_token else '' }} \
-e WEBUI_SECRET_KEY={{ open_webui_key }} \
-e ENABLE_QUERIES_CACHE=True \
-v open-webui:/app/backend/data \
$image
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | open-webui | Script | Finish
ansible.builtin.copy:
dest: /etc/sv/open-webui/finish
content: |
{{ runit_finish_sleep }}
echo "FINISH: Stopping..."
docker stop open-webui &&
echo "FINISH: Container stopped" ||
echo "FINISH: Container already stopped."
echo "FINISH: Deleting..."
docker rm -f open-webui &&
echo "FINISH: Container deleted" ||
echo "FINISH: Container already deleted."
echo "FINISH: Done!"
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | open-webui | Script | Log
ansible.builtin.copy:
dest: /etc/sv/open-webui/log/run
content: |
#!/bin/sh -e
exec svlogd -tt /var/log/open-webui
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | open-webui | Script | Log Config
ansible.builtin.copy:
dest: /var/log/open-webui/config
content: |
{{ runit_log_config_100mb }}
mode: "0755"
# Open WebUI - End #
# Open Terminal - Start #
- name: Workstation | Linux | Software | LLMs | runit | open-terminal | Dir | /etc/sv
ansible.builtin.file:
name: "/etc/sv/open-terminal/log"
state: directory
owner: root
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | open-terminal | Dir | /var/log
ansible.builtin.file:
name: "/var/log/open-terminal"
state: directory
owner: root
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | open-terminal | Dir | /usr/local/etc
ansible.builtin.file:
name: "/usr/local/etc/open-terminal"
state: directory
owner: root
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | open-terminal | AI-Sandbox | Dir
ansible.builtin.file:
dest: "{{ user_user.home }}/AI-Sandbox"
state: directory
owner: "{{ user }}"
group: "{{ user }}"
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | open-terminal | AI-Sandbox | Env
ansible.builtin.blockinfile:
path: "{{ user_user.home }}/AI-Sandbox/{{ item }}"
marker: "# {mark} | MANAGE BY ANSIBLE | Env Vars"
content: |
if [[ -d "$HOME/bin" ]]; then
export PATH="$HOME/bin:$PATH"
fi
export ANDROID_HOME="{{ user_user.home }}/SDKs/Android/Sdk"
export GRADLE_USER_HOME={{ user_user.home }}/.gradle
export PATH="$ANDROID_HOME/platform-tools:$PATH"
export PATH="$ANDROID_HOME/cmdline-tools/latest/bin:$PATH"
export FLUTTER_HOME="{{ user_user.home }}/SDKs/Flutter/flutter"
export PUB_CACHE={{ user_user.home }}/.pub-cache
export PATH="$FLUTTER_HOME/bin:$PATH"
export JAVA_HOME="{{ user_user.home }}/SDKs/Java"
export PATH="$JAVA_HOME/bin:$PATH"
create: false
backup: false
owner: "{{ user }}"
group: "{{ user }}"
mode: "0755"
loop:
- ".bashrc"
- name: Workstation | Linux | Software | LLMs | runit | open-terminal | AI-Sandbox | Defaults
ansible.builtin.set_fact:
jdk_jvm: "{{ jdk_jvm | default('/usr/lib/jvm') }}"
jdk_etc: "{{ jdk_etc | default('/etc/java-openjdk') }}"
jdk_etc17: "{{ jdk_etc17 | default('/etc/java17-openjdk') }}"
jdk_etc21: "{{ jdk_etc21 | default('/etc/java21-openjdk') }}"
jdk_etc25: "{{ jdk_etc25 | default('/etc/java25-openjdk') }}"
- name: Workstation | Linux | Software | LLMs | runit | open-terminal | AI-Sandbox | AGENTS.md
ansible.builtin.copy:
dest: "{{ user_user.home }}/AI-Sandbox/AGENTS.md"
content: |
# Environment
You are running in a Docker container called Open Terminal. We are
communicating via Open WebUI, also being run in a Docker
container. You are the user `user` and have been granted access to
the designer's Code and SDKs via mount at `{{ user_user.home }}`.
Host system is typically (but not always) a systemd-free distro
such as Artix or Devuan. User likes the `runit` service manager.
`elogind` is used rather than systemd's logind. Desktop is i3 and
display manager is lightdm. Servers are run on Debian and Docker
containers are typically debian:trixie-slim with a Dockerfile
called by compose.yml which adds the necessary packages, sometimes
building from source if the Debian executable is too old.
If the `$GOT_EM` environment variable is `true` then all the
necessary packages for compiling Android and Flutter were
installed. If it is `false` then they were not installed, and
compiling will fail. Please check this before trying and alert me
if they need added. If we don't have Internet, then ask me to
compile or run whatever command is needed on the host system.
# Rules
Never use any `git` subcommands that change the repo, including
but not limited to `add`, `commit`, `pull`, `push`, `branch`,
`switch`, etc. The user is who controls the respository changes.
You may use informational git subcommands such as `git status`,
and may occasionally be asked to run `git clone` type commands.
If any programs need installed in order to work on a project, we
need to document that in an `install.sh` script, as well as add it
to `~/PACKAGES.txt` so that we can ensure they stay installed.
`install.sh` scripts should be for both `apt` systems (Debian,
Ubuntu) and pacman system (Arch, Artix). using distribution
package managers is preferable over `pip` and `npm` when
possible, unless the project is actually Python or Node.js.
The user manages the `{{ user_user.home }}/SDKs` directory. Do not
modify it. Instead, ask if anything needs updated or added.
Never manually edit, modify, delete, alter, replace, etc any
files under system folders such as `/usr`. Anything under `/` is
managed by the system. If you need assistance changing anything
such as `/usr/lib/jvm`, please ask the user and halt all further
operations until you have received feedback.
# Effort
It is never okay to brute force a solution if something is not
working well. it is better to ask the user for assistance or alert
them that you've encountered blockers and then cease the process
until you've received feedback. This especially relates to when
system errors occur which are not related to the source code, such
as a compiler failing to be found or an SDK version mismatch. If
the error is with the source code though, troubleshooting and code
changes should continue until the program is compiling properly.
For CLI projects such as script languages, the final result should
always be tested both to run successfully and have the proper
output. All test cases should be analyzed and walked through,
which will help ensure all the requirements have been met.
For GUI programs (such as Android, Flutter, Kotlin Multiplatform,
Kivy, etc), the project should be tested to both compile & build
successfully and also generate the proper installation file (APK,
Linux Bundle). Once that is confirmed, the user/desginer will test
that the application meets the proper requirements.
# Preferences
You have free reign over your home directory. Feel free to
create memory files or anything else which would benefit you.
Everything we create should have both Desktop Linux and Android
in mind. Windows, MacOS, and iOS will never be compiled for, but
if we can do things in a way which makes it easy for others to
fork the project and add the support themselves, then that's best.
# Versions
These are the versions of plugins and languages we need to use.
If you know of an important upgrade from them, please let me know.
## Android / Flutter
- Gradle: 9.8.0
- AGP: 9.4.1
- Kotlin: 2.4.20
- Java: JDK 17
- Flutter (plugin loader): 1.0.0
Regardless of whether AGP bundles Kotlin, we need to specify it to
avoid things like "Your project's Kotlin version (2.2.10) is lower
than Flutter's minimum supported version of 2.2.20. Please upgrade
your Kotlin version." This is exactly why we use these versions,
plus it helps document what's actually being used over guessing.
backup: false
owner: "{{ user }}"
group: "{{ user }}"
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | open-terminal | Script | Compose YAML
ansible.builtin.copy:
dest: /usr/local/etc/open-terminal/compose.yml
content: |
#exec docker run \
# --name open-terminal \
# --network=host \
# -v {{ user_user.home }}/AI-Sandbox:/home/user \
# -v {{ user_user.home }}/Code:{{ user_user.home }}/Code \
# -v {{ user_user.home }}/SDKs:{{ user_user.home }}/SDKs \
# -v {{ jdk_jvm }}:{{ jdk_jvm }}:ro \
# -v {{ jdk_etc }}:{{ jdk_etc }}:ro \
# -v {{ jdk_etc17 }}:{{ jdk_etc17 }}:ro \
# -v {{ jdk_etc21 }}:{{ jdk_etc21 }}:ro \
# -v {{ jdk_etc25 }}:{{ jdk_etc25 }}:ro \
# -v /etc/hosts:/etc/hosts:ro \
# -v {{ user_user.home }}/.gradle:{{ user_user.home }}/.gradle \
# -v {{ user_user.home }}/.pub-cache:{{ user_user.home }}/.pub-cache \
# -e OPEN_TERMINAL_FILE_BROWSER_ROOT="/home/user" \
# -e OPEN_TERMINAL_API_KEY="{{ open_terminal_key }}" \
# -e OPEN_TERMINAL_PACKAGES="$packages" \
# -e GOT_EM="$GOT_EM" \
# $image run --port {{ open_terminal_port }}
networks:
{{ llm_docker_network }}:
name: {{ llm_docker_network }}
external: true
services:
open-terminal:
build:
context: ./
image: hyperling-open-terminal:latest
container_name: open-terminal
ports:
- {{ open_terminal_port }}:{{ open_terminal_port }}
networks:
- {{ llm_docker_network }}
#dns:
# ## Mullvad
# ## (https://mullvad.net/en/help/dns-over-https-and-dns-over-tls)
# #- 194.242.2.9 # all.dns.mullvad.net
# ##- 194.242.2.8 # not real (yet?)
# ##- 194.242.2.7 # not real (yet?)
# #- 194.242.2.6 # family.dns.mullvad.net
# #- 194.242.2.5 # extended.dns.mullvad.net
# #- 194.242.2.4 # base.dns.mullvad.net
# #- 194.242.2.3 # adblock.dns.mullvad.net
# - 194.242.2.2 # dns.mullvad.net
# #- 194.242.2.1 # not real (yet?)
# #- 194.242.2.0 # not real (yet?)
# ## Cloudflare
# - 1.0.0.1
# - 1.1.1.1
command: run --port {{ open_terminal_port }}
environment:
- OPEN_TERMINAL_FILE_BROWSER_ROOT=/home/user
- OPEN_TERMINAL_API_KEY={{ open_terminal_key }}
- OPEN_TERMINAL_PACKAGES=
- GOT_EM=true
volumes:
- {{ user_user.home }}/AI-Sandbox:/home/user
- {{ user_user.home }}/Code:{{ user_user.home }}/Code
- {{ user_user.home }}/SDKs:{{ user_user.home }}/SDKs
- {{ jdk_jvm }}:{{ jdk_jvm }}:ro
- {{ jdk_etc }}:{{ jdk_etc }}:ro
- /etc/ssl/certs/java:/etc/ssl/certs/java:ro
- /etc/ca-certificates:/etc/ca-certificates:ro
- {{ jdk_etc17 }}:{{ jdk_etc17 }}:ro
- {{ jdk_etc21 }}:{{ jdk_etc21 }}:ro
- {{ jdk_etc25 }}:{{ jdk_etc25 }}:ro
#- /etc/resolv.conf:/etc/resolv.conf:ro
- {{ user_user.home }}/.gradle:{{ user_user.home }}/.gradle
- {{ user_user.home }}/.pub-cache:{{ user_user.home }}/.pub-cache
owner: root
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | open-terminal | Script | Dockerfile
ansible.builtin.copy:
dest: /usr/local/etc/open-terminal/Dockerfile
content: |
FROM ghcr.io/open-webui/open-terminal:latest
USER root
RUN echo "System"
RUN apt-get update && apt-get install -y \
ca-certificates git curl bash sudo wget ansible ansible-lint
RUN echo "Android"
RUN apt-get update && apt-get install -y \
libc6:amd64 libncurses-dev \
libstdc++6 lib32z1 libbz2-1.0 android-udev-rules
# Debian removed JDK 17, continue serving from the host.
# default-jdk openjdk-17-jdk
RUN echo "Flutter"
RUN apt-get update && apt-get install -y \
curl git unzip xz-utils zip libglu1-mesa libglu1-mesa
RUN echo "Flutter (Linux)"
RUN apt-get update && apt-get install -y \
clang cmake git ninja-build pkg-config libgtk-3-dev \
liblzma-dev libstdc++-12-dev
RUN echo "Flutter (Android)"
RUN apt-get update && apt-get install -y \
libc6:amd64 libstdc++6 lib32z1 libbz2-1.0
RUN echo "Ansible"
RUN apt-get update && apt-get install -y \
ansible ansible-lint
USER user
owner: root
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | open-terminal | Script | env
ansible.builtin.copy:
dest: /usr/local/etc/open-terminal/.env
content: |
COMPOSE_BAKE=true
owner: root
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | open-terminal | Script | Run
ansible.builtin.copy:
dest: /etc/sv/open-terminal/run
content: |
#!/bin/sh
exec 2>&1
# Wait for Docker to start up.
while [ ! -S /var/run/docker.sock ]; do sleep 1; done
# Ensure the old container is stopped.
echo "RUN: Stopping..."
docker compose \
-f /usr/local/etc/open-terminal/compose.yml down &&
echo "RUN: Container stopped." ||
echo "RUN: Container already stopped."
# Check for any new updates in the background.
(
while true; do
sleep 600
echo "RUN: Checking for updates..."
docker compose \
-f /usr/local/etc/open-terminal/compose.yml \
build --pull
done
) &
LOOP_PID=$!
# Ensure cleanup on any exit (normal or signal from runit).
trap 'kill "$LOOP_PID" 2>/dev/null; wait "$LOOP_PID" 2>/dev/null' EXIT
export GOT_EM=true
# Start the open-terminal program.
# https://github.com/open-webui/open-terminal#configuration
set -e
docker compose -f /usr/local/etc/open-terminal/compose.yml up
owner: root
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | open-terminal | Script | Finish
ansible.builtin.copy:
dest: /etc/sv/open-terminal/finish
content: |
{{ runit_finish_sleep }}
echo "FINISH: Stopping..."
docker compose -f /usr/local/etc/open-terminal/compose.yml down &&
echo "FINISH: Container stopped" ||
echo "FINISH: Container already stopped."
echo "FINISH: Done!"
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | open-terminal | Script | Log
ansible.builtin.copy:
dest: /etc/sv/open-terminal/log/run
content: |
#!/bin/sh -e
exec svlogd -tt /var/log/open-terminal
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | open-terminal | Script | Log Config
ansible.builtin.copy:
dest: /var/log/open-terminal/config
content: |
{{ runit_log_config_100mb }}
mode: "0755"
# Open Terminal - End #
# Swap Stopper - Start #
- name: Workstation | Linux | Software | LLMs | runit | swap-stopper | Script | Executable
ansible.builtin.copy:
dest: "{{ global_bin }}/llm_swap_stopper.sh"
src: files/scripts/llm_swap_stopper.sh
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | swap-stopper | Dir | /etc/sv
ansible.builtin.file:
name: "/etc/sv/swap-stopper/log"
state: directory
owner: root
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | swap-stopper | Dir | /var/log
ansible.builtin.file:
name: "/var/log/swap-stopper"
state: directory
owner: "{{ user }}"
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | swap-stopper | Script | Run
ansible.builtin.copy:
dest: /etc/sv/swap-stopper/run
content: |
#!/bin/sh -e
exec 2>&1
exec {{ global_bin }}/llm_swap_stopper.sh
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | swap-stopper | Script | Finish
ansible.builtin.copy:
dest: /etc/sv/swap-stopper/finish
content: |
{{ runit_finish_sleep }}
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | swap-stopper | Script | Log
ansible.builtin.copy:
dest: /etc/sv/swap-stopper/log/run
content: |
#!/bin/sh -e
exec svlogd -tt /var/log/swap-stopper
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | swap-stopper | Script | Log Config
ansible.builtin.copy:
dest: /var/log/swap-stopper/config
content: |
{{ runit_log_config_1mb }}
mode: "0755"
# Swap Saver - End #
# Qwen Serve - Start #
- name: Workstation | Linux | Software | LLMs | runit | qwen-code | Dir | /etc/sv
ansible.builtin.file:
name: "/etc/sv/{{ qwen_code }}/log"
state: directory
owner: root
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | qwen-code | Dir | /var/log
ansible.builtin.file:
name: "/var/log/{{ qwen_code }}"
state: directory
owner: "{{ user }}"
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | qwen-code | Script | Run
ansible.builtin.copy:
dest: /etc/sv/{{ qwen_code }}/run
content: |
#!/bin/sh -e
exec 2>&1
export QWEN_STREAM_IDLE_TIMEOUT_MS=6000000
{{ ('export QWEN_SERVER_TOKEN=' ~ qwen_code_key) if qwen_code_key else '' }}
exec chpst -u {{ user }} -C {{ user_user.home }}/Code qwen serve
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | qwen-code | Script | Finish
ansible.builtin.copy:
dest: /etc/sv/{{ qwen_code }}/finish
content: |
{{ runit_finish_sleep }}
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | qwen-code | Script | Log
ansible.builtin.copy:
dest: /etc/sv/{{ qwen_code }}/log/run
content: |
#!/bin/sh -e
exec svlogd -tt /var/log/{{ qwen_code }}
mode: "0755"
- name: Workstation | Linux | Software | LLMs | runit | qwen-code | Script | Log Config
ansible.builtin.copy:
dest: /var/log/{{ qwen_code }}/config
content: |
{{ runit_log_config_10mb }}
mode: "0755"
# Qwen Serve - End #
when: ansible_service_mgr == "runit"
# Services - Start #
- name: Workstation | Linux | Software | LLMs | Services | Enable
ansible.builtin.service:
name: "{{ item.name }}"
state: "{{ 'started' if item.status else 'stopped' }}"
enabled: "{{ item.status }}"
loop:
- { 'name': "{{ ollama }}" , 'status': "{{ ollama_enabled }}" }
- { 'name': "{{ zeroclaw }}" , 'status': "{{ zeroclaw_enabled }}" }
- { 'name': 'docker' , 'status': true }
- { 'name': 'open-webui' , 'status': "{{ open_webui_enabled }}" }
- { 'name': 'open-terminal' , 'status': "{{ open_terminal_enabled }}" }
- { 'name': 'swap-stopper' , 'status': true }
- { 'name': '{{ qwen_code }}', 'status': "{{ qwen_code_enabled }}" }
- name: Workstation | Linux | Software | LLMs | Services | Verify | Critical
ansible.builtin.uri:
url: "{{ item }}"
status_code: 200
timeout: 5
register: health
until: health.status == 200
retries: 12
delay: 5
loop:
- "{{ 'http://127.0.0.1:11434' if ollama_enabled else dummy_website }}"
- name: Workstation | Linux | Software | LLMs | Services | Verify | Okay to Fail
ansible.builtin.uri:
url: "{{ item }}"
status_code: 200
timeout: 5
register: health
until: health.status == 200
retries: 2
delay: 5
loop:
- "{{ 'http://127.0.0.1:42617/health' if zeroclaw_enabled else dummy_website }}"
- "{{ 'http://127.0.0.1:' ~ open_webui_port ~ '/health' if open_webui_enabled else dummy_website }}"
- "{{ 'http://127.0.0.1:' ~ open_terminal_port ~ '/health' if open_terminal_enabled else dummy_website }}"
- "{{ 'http://127.0.0.1:4170' if qwen_code_enabled else dummy_website }}"
ignore_errors: true
# Services - End #
# Models - Start #
- name: Workstation | Linux | Software | LLMs | Models | ollama | User Shell Functions
ansible.builtin.blockinfile:
path: "{{ user_user.home }}/{{ item }}"
marker: "# {mark} ANSIBLE MANAGED BLOCK | ollama | Manual Script"
content: |
function ollama_llms() {
{% for llm in llm_installs %}
echo "{{ llm.model }}"
{% endfor %}
}
function ollama_pull() {
echo "Manually Pulling Models"
for model in `ollama_llms`; do
echo -e "\n\n Starting 'ollama pull $model' \n\n"
ollama pull $model
status=$?
echo "Finished with status $status."
done
echo "Done!!"
}
alias pull-llms="ollama_pull "
alias pull-models="ollama_pull "
alias pull-ai="ollama_pull "
function ollama_chat() {
ollama run {{ 'qwen3.8:27b' if ansible_memtotal_mb > 32000 else 'qwen3.5:4b' }}
}
alias ollama-chat="ollama_chat "
alias chat="ollama_chat "
function ollama_code() {
ollama run {{ 'qwen3.8:27b' if ansible_memtotal_mb > 32000 else 'qwen2.5-coder:7b' }}
}
alias ollama-code="ollama_code "
function ollama_purge() {
echo "TBD: Purge all models not in llm_installs."
}
alias ollama-purge="ollama_purge "
alias ollama-clean="ollama_purge "
alias ollama-remove="ollama_purge "
alias ollama-rm="ollama_purge "
alias remove-models="ollama_purge "
alias rm-models="ollama_purge "
alias purge-models="ollama_purge "
function restart_ai() {
echo "Restarting AI Software"
for sv in ollama zeroclaw open-webui open-terminal qwen-code swap-stopper; do
echo -e "\n $sv \n"
sudo sv restart $sv
done;
}
alias restart-ai="restart_ai "
alias restart-llm="restart_ai "
alias restart-llms="restart_ai "
alias restart-models="restart_ai "
alias reload-ai="restart_ai "
state: present
create: false
backup: true
loop:
- .bashrc
- .zshrc
- name: Workstation | Linux | Software | LLMs | Models | ollama | Manage
block:
- name: Workstation | Linux | Software | LLMs | Models | ollama | Manage | Message
ansible.builtin.debug:
msg: <-
The next task may take hours, if not days. If you'd like a status
bar to know what's being worked on and its progress, I highly
recommed reloading your RC file and running `pull-models`. That
will give you a much better idea of what's going on.
- name: Workstation | Linux | Software | LLMs | Models | ollama | Manage | Install
ansible.builtin.shell: |
set -o pipefail
item='{{ item.model }}'
if ollama list | grep -q $item; then
echo "OKAY" &&
return
else
ollama pull $item 1>/dev/null &&
echo "INSTALLED" &&
return
fi
echo "FAILED"
loop: "{{ llm_installs }}"
register: shell_check
changed_when: "'INSTALLED' in shell_check.stdout"
failed_when: "'FAILED' in shell_check.stdout"
become: true
become_user: "{{ user }}"
when: manage_models
and llm_installs | length > 0
- name: Workstation | Linux | Software | LLMs | Models | ollama | Manage | Remove
ansible.builtin.shell: |
set -o pipefail
item='{{ item.model }}'
if ollama list | grep -q $item; then
ollama rm $item &&
echo "INSTALLED" &&
return
else
echo "OKAY" &&
return
fi
echo "FAILED"
loop: "{{ llm_removals }}"
register: shell_check
changed_when: "'INSTALLED' in shell_check.stdout"
failed_when: "'FAILED' in shell_check.stdout"
become: true
become_user: "{{ user }}"
when: llm_removals | length > 0
when: manage_models
- name: Workstation | Linux | Software | LLMs | Models | ollama | Get Models
ansible.builtin.shell:
cmd: |
set -o pipefail
ollama ls | tail -n +2 | awk '{print $1}'
register: llm_models
changed_when: llm_models.rc == 0
- name: Workstation | Linux | Software | LLMs | Models | ollama | Get Models | Result
ansible.builtin.debug:
var: llm_models
# Cheat Sheet: https://docs.zeroclaw.com/master/en/providers/configuration.html
- name: Workstation | Linux | Software | LLMs | Models | zeroclaw | Add Providers
ansible.builtin.blockinfile:
path: "{{ user_user.home }}/.zeroclaw/config.toml"
marker: "# {mark} ANSIBLE MANAGED BLOCK | zeroclaw | Providers"
insertafter: EOF
block: |
{% for llm in llm_installs %}
{% if llm.model in llm_models.stdout %}
{% if "chat" in llm.purpose %}
[providers.models.ollama.{{ llm.provider }}_{{ 'CODE' if "code" in llm.purpose else 'CHAT'
}}{{ '' if llm.model in llm_models.stdout else '-MISSING'
}}]
model = "{{ llm.model }}"
timeout_secs = 3600 # 1 hour, default is 120 (2 minutes), issue happens between 5 and 6 minutes though.
context_window = {{ [llm.context] | min | int }}
max_tokens = {{ [(llm.context / 8), 16000] | min | int }}
{% endif %}
{% endif %}
{% endfor %}
state: present
create: false
backup: true
when: zeroclaw_enabled
- name: Workstation | Linux | Software | LLMs | Models | Continue | Config
ansible.builtin.copy:
dest: "{{ user_user.home }}/.continue/config.yaml"
content: |
# Maintained by `env-ansible/tasks/workstation/linux/software/llm.yml`
name: Main Config
version: 1.0.0
schema: v1
models:
# | Role | `contextLength` | `maxTokens` | Notes |
# |------|-----------------|-------------|-------|
# | Chat / Edit | 131072 (128K) | 16384 - 65536 | Qwen recommends 65,536 output for instruct models; use lower if VRAM is tight |
# | Autocomplete | 32768 - 65536 | 1024 - 4096 | Keep low for speed; autocomplete doesn't need long outputs |
{% for llm in llm_installs %}
{% if llm.model in llm_models.stdout %}
{% if "code" in llm.purpose %}
- name: {{ llm.provider }}_{{ 'CODE' if "code" in llm.purpose else 'CHAT'
}}{{ '' if llm.model in llm_models.stdout else '-MISSING'
}}
provider: ollama
model: {{ llm.model }}
defaultCompletionOptions:
contextLength: {{ [llm.context] | min | int }}
maxTokens: {{ [(llm.context / 8), 16000] | min | int }}
{% endif %}
{% endif %}
{% endfor %}
mode: "0644"
when: codium_config
#- name: {{ llm.provider }}_AUTO{{ '' if llm.model in llm_models.stdout else '-MISSING'
# }}
# provider: ollama
# model: {{ llm.model }}
# defaultCompletionOptions:
# contextLength: {{ [llm.context] | min | int }}
# maxTokens: {{ [(llm.context / 8), 4000] | min | int }}
# roles:
# - autocomplete
- name: Workstation | Linux | Software | LLMs | Models | qwen-code | Config
ansible.builtin.copy:
dest: "{{ item }}"
content: |
{
"$version": 4,
"env": {
"OLLAMA_API_KEY": "ollama"
},
"model": {
"name": "qwen2.5-coder:3b",
"baseUrl": "http://127.0.0.1:11434/v1"
},
"modelProviders": {
"openai": [
{% for llm in llm_installs %}
{
"name": "{{ llm.provider
}}_{{ 'CODE' if "code" in llm.purpose else 'CHAT'
}}{{ '' if llm.model in llm_models.stdout else '-MISSING'
}}"
,"baseUrl": "http://127.0.0.1:11434/v1"
,"envKey": "OLLAMA_API_KEY"
,"id": "{{ llm.model }}"
,"generationConfig": {
"timeout": 600000
,"streamIdleTimeoutMs": 600000
,"contextWindowSize": {{ [llm.context] | min | int }}
,"maxRetries": 3
}
}{{ ',' if not loop.last else '' }}
{% endfor %}
]
},
"security": {
"auth": {
"baseUrl": "http://127.0.0.1:11434/v1",
"selectedType": "openai"
}
},
"ui": {
"autoModeAcknowledged": true
}
}
mode: "0644"
loop:
- "{{ user_user.home }}/.qwen/settings.json"
# Not Needed
#- "{{ user_user.home }}/Code/.qwen/settings.json"
when: qwen_code_enabled
#"reasoning": {
# "effort": "medium"
#},
#"samplingParams": {
# "max_tokens": {{ [(llm.context / 8), 16000] | min | int }}
# ,"temperature": 0.7
# ,"top_p": 0.8
#}
- name: Workstation | Linux | Software | LLMs | Models | qwen-code | Config | Validate
ansible.builtin.shell: |
set -o pipefail
cat {{ user_user.home }}/.qwen/settings.json | jq
register: shell_check
changed_when: shell_check.rc == 0
failed_when: shell_check.rc != 0
when: qwen_code_enabled
# Models - End #
when: ansible_pkg_mgr == "pacman"