diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e927727..be0bb8e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,7 @@ jobs: - name: Install Lex toolchain run: | set -euxo pipefail - LEX_VERSION="0.10.7" + LEX_VERSION="0.10.10" curl -fsSL -o /tmp/lex.tgz \ "https://github.com/alpibrusl/lex-lang/releases/download/v${LEX_VERSION}/lex-v${LEX_VERSION}-x86_64-unknown-linux-gnu.tar.gz" tar -xzf /tmp/lex.tgz -C /tmp/ diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index b750b03..1e41594 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -27,7 +27,7 @@ jobs: - name: Install lex if: steps.ver.outputs.changed == 'true' run: | - curl -fsSL "https://github.com/alpibrusl/lex-lang/releases/download/v0.10.7/lex-v0.10.7-x86_64-unknown-linux-gnu.tar.gz" \ + curl -fsSL "https://github.com/alpibrusl/lex-lang/releases/download/v0.10.10/lex-v0.10.10-x86_64-unknown-linux-gnu.tar.gz" \ | tar -xz --strip-components=1 -C /usr/local/bin - name: Mint JWT + publish diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..52f3028 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,74 @@ +# lex-code — Agent Guidelines + +A Lex-native coding assistant — agents, tools, TUI, and A2A/ACP +servers, all written in Lex. Read `lex agent-guidelines` in full +before writing code. The four highest-leverage discipline rules: + +1. **Narrow effects, always.** `fn foo() -> [fs_write("/tmp/x")] T`, + not `[fs_write]`. If the type checker rejects, narrow the body. +2. **Repair, don't regenerate.** `lex check --output json` → + `lex repair --apply`. Only regenerate after two failed repairs. +3. **`examples {}` blocks on every pure fn.** They fold into the + SigId and run at `lex check` time. +4. **Use the stdlib.** `std.crypto` for hashing, `std.regex` over + hand-rolled scanners. + +## The loop + +```sh +lex pkg install # resolves lex-llm, lex-agent, lex-trail, lex-spec, lex-schema, ... +lex check # this repo has no tests/ dir — see below +lex fmt --check src/ +``` + +There is no `tests/` directory. CI type-checks every file in `src/` +individually, then gates on the manifesto demo examples: + +```sh +lex check examples/manifesto_parallel.lex # must pass +lex check examples/manifesto_parallel_bad.lex # must FAIL (missing [concurrent]) +bash examples/manifesto_semantic_diff/run.sh # semantic diff must surface the effect-row change +``` + +Treat those three as the regression suite until a real `tests/` +directory exists. + +## Project-specific overrides — lex-code + +- **Two call sites per agent mode, times seven files.** Each + `src/agents/*.lex` (build, explore, plan, refactor, review, + spec_agent, test_agent) defines one `AgentLoop`-builder per + provider: `agent()` (Anthropic), `openai_agent()`, `mistral_agent()`, + `google_agent()`, `vertex_agent()`, `ollama_agent()`, + `vllm_agent()`, `litellm_agent()`, `opencode_agent()`. Adding a + provider means adding the function to **all seven** files *and* a + matching branch in `src/server/session.lex`'s `pick_agent` — a + missing branch is a silent fallthrough, not a type error. +- **`select_provider_tag` (TUI) and `pick_agent` (session) must move + together.** A new `--flag` in `src/tui/main.lex` with no matching + `"tag" => ...` arm in `session.lex`'s `pick_agent` silently no-ops + rather than failing to compile — always change both in the same + commit. +- **Local-model tool budgets are curated, not accidental.** + `ollama_agent`/`litellm_agent` use `tools.minimal_tools()` (build) + or `[]` (every other mode) instead of `all_tools()` — 38 tool + schemas overwhelm small local models (see the local-model + compatibility table in `README.md`). Don't widen this without + re-testing against a real local model. +- **Permission specs gate tool *visibility*, not just execution.** + `ag.with_permission_gate(base, rules._permission())` (from + `src/permissions/rules.lex`) filters the tool list at construction + time. Every provider variant for a given mode must be wrapped with + the *same* permission spec — don't special-case one provider's tool + list without updating its spec. +- **`src/server/session.lex` is the single turn-handling path.** The + TUI, the A2A server (`src/server/api.lex`), and the ACP server + (`src/server/acp.lex`) are three transports over the same + `Session`/`run_turn_with_provider`. Extend `session.lex`, don't + duplicate turn logic into a transport file. +- **LiteLLM config lives in `litellm/`.** `config.yaml` + + `docker-compose.yml` are the reference proxy setup (Ollama local + models, vLLM, ChatGPT/Claude subscription passthrough, OpenCode Go) + — kept in sync with `lex-loom`'s own `litellm/` directory since both + repos are meant to work against the same proxy. Don't fork the model + list; add new entries to both repos together. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..1148342 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,22 @@ +# CLAUDE.md — lex-code + +> Copy this file into the root of any Lex project repository as +> `CLAUDE.md` (read by Claude Code), `AGENTS.md` (read by Cursor / +> Aider / Codex CLI / Copilot CLI), or both. This repo ships both, +> kept in sync — read `AGENTS.md` in full before writing code. + +This repository is a **Lex** project — a Lex-native coding assistant +(agents, tools, TUI, A2A/ACP servers). `AGENTS.md` carries the full +discipline, including why this repo has no `tests/` directory and how +provider support is wired across seven agent files plus +`session.lex`. + +## The loop + +```sh +lex pkg install +lex check # no tests/ dir — see AGENTS.md for the CI gate +lex fmt --check src/ +``` + +See `AGENTS.md` for the full discipline. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6102dcd --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +European Union Public Licence v. 1.2 (EUPL-1.2) + +Copyright (c) 2026 lex-code contributors + +Licensed under the EUPL, Version 1.2 only (the "Licence"); you may not +use this work except in compliance with the Licence. + +You may obtain a copy of the Licence at: + + https://joinup.ec.europa.eu/sites/default/files/custom-page/attachment/eupl_v1.2_en.pdf + https://eupl.eu/1.2/en/ (HTML, all 23 EU languages) + https://spdx.org/licenses/EUPL-1.2.html (SPDX identifier: EUPL-1.2) + +Unless required by applicable law or agreed to in writing, software +distributed under the Licence is distributed on an "AS IS" basis, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +implied. See the Licence for the specific language governing +permissions and limitations under the Licence. + +The EUPL-1.2 is a copyleft licence approved by the European Commission +and is compatible with several other open-source licences listed in +its Appendix (including GPL-2.0, GPL-3.0, AGPL-3.0, LGPL-2.1, LGPL-3.0, +MPL-2.0, EPL-1.0, CeCILL-2.0/2.1, OSL-2.1/3.0, and CC-BY-SA-3.0). diff --git a/README.md b/README.md index f6daf7b..1f88a12 100644 --- a/README.md +++ b/README.md @@ -89,6 +89,7 @@ lex-code --plan --ollama "how should we structure the session module?" | `--litellm` | LiteLLM proxy | `$LITELLM_MODEL` | none (proxy handles keys) | | `--ollama` | Ollama (local, native API) | `$OLLAMA_MODEL` | none | | `--vllm` | vLLM (local/remote) | `$VLLM_MODEL` | none | +| `--opencode` | OpenCode Go plan (cloud, direct) | `$OPENCODE_MODEL` | `OPENCODE_API_KEY` | ### Ollama @@ -117,13 +118,40 @@ VLLM_MODEL=deepseek-ai/DeepSeek-Coder-V2-Lite-Instruct \ `VLLM_MODEL` defaults to `mistralai/Mistral-7B-Instruct-v0.3`. `VLLM_BASE_URL` defaults to `http://localhost:8000/v1/chat/completions`. -### LiteLLM (local models via proxy) +### OpenCode Go plan -[LiteLLM](https://github.com/BerriAI/litellm) is the recommended path for running local models. It provides an OpenAI-compatible endpoint over any backend (Ollama, vLLM, MLX, …), which gives cleaner tool calling than the native Ollama wire format. +[OpenCode Go](https://opencode.ai/docs/zen) bundles cloud access to several open-weight coding models (DeepSeek, Qwen3, Kimi, GLM, MiniMax, MiMo) behind one subscription key. Two ways to reach it — same key either way: ```sh -# start the LiteLLM proxy (config at project root) -litellm --config litellm_config.yaml --port 4000 +# native (direct to the Go endpoint, no proxy) +export OPENCODE_API_KEY=$(cat ~/.credentials/opencode/key | tr -d '\n') +lex run --allow-effects env,io,net,llm,proc,sql,fs_write,time \ + src/tui/main.lex main -- --opencode "implement list.zip" + +# override the default model (kimi-k2.7-code) +OPENCODE_MODEL=qwen3.7-max \ + lex run --allow-effects env,io,net,llm,proc,sql,fs_write,time \ + src/tui/main.lex main -- --opencode "implement list.zip" + +# via the LiteLLM proxy instead (shares one proxy + model list with lex-loom — see below) +LITELLM_MODEL=deepseek-v4-flash \ + lex run --allow-effects env,io,net,llm,proc,sql,fs_write,time \ + src/tui/main.lex main -- --litellm "implement list.zip" +``` + +`OPENCODE_MODEL` accepts any Go-plan model id (see `litellm/config.yaml`'s "OpenCode Go plan" section for the full list). `OPENCODE_BASE_URL` overrides the endpoint if you're routing through a local reasoning proxy instead of hitting `opencode.ai` directly. + +### LiteLLM (local models + OpenCode Go via proxy) + +[LiteLLM](https://github.com/BerriAI/litellm) is the recommended path for running local models, and the only path that gives OpenCode Go's thinking-mode models correct `merge_reasoning_content_in_choices` handling. It provides an OpenAI-compatible endpoint over any backend (Ollama, vLLM, OpenCode Go, MLX, …), which gives cleaner tool calling than the native Ollama wire format. + +This repo ships a ready-to-run proxy config at `litellm/config.yaml` + `litellm/docker-compose.yml` — kept in sync with [lex-loom](https://github.com/alpibrusl/lex-loom)'s own `litellm/` directory (same model list, same OpenCode Go entries) so both repos can point at one shared proxy instance. + +```sh +# start the bundled proxy +cd litellm +ANTHROPIC_API_KEY=... OPENAI_API_KEY=... OPENCODE_API_KEY=... docker compose up -d +cd .. # run lex-code against qwen3-coder:30b (recommended local model) LITELLM_MODEL=qwen3-coder:30b \ @@ -135,6 +163,11 @@ LITELLM_MODEL=qwen3-coder:30b \ lex run --allow-effects env,io,net,llm,proc,sql,fs_write,time \ src/tui/main.lex main -- --litellm "implement list.zip" +# OpenCode Go through the proxy instead of native --opencode +LITELLM_MODEL=kimi-k2.7-code \ + lex run --allow-effects env,io,net,llm,proc,sql,fs_write,time \ + src/tui/main.lex main -- --litellm "implement list.zip" + # override the proxy URL (default: http://localhost:4000) LITELLM_BASE_URL=http://gpu-box:4000 \ LITELLM_MODEL=qwen3-coder:30b \ @@ -142,9 +175,11 @@ LITELLM_MODEL=qwen3-coder:30b \ src/tui/main.lex main -- --litellm ``` -`LITELLM_MODEL` is the model name as it appears in your `litellm_config.yaml` `model_name` field. +`LITELLM_MODEL` is the model name as it appears in `litellm/config.yaml`'s `model_name` field. `LITELLM_BASE_URL` defaults to `http://localhost:4000`. +Running against a standalone LiteLLM install instead of the bundled compose file works the same way — point `litellm --config --port 4000` at any config with the model names you use. + #### Local model compatibility Tested on [lex-code fizzbuzz bootstrap](src/bootstrap/fizzbuzz_lex.lex) — task: write `fizzbuzz.lex` with `fn fizzbuzz(n :: Int) -> List[Str]` + 4 unit tests, `lex check` clean, `run_all` returns 0. @@ -406,6 +441,13 @@ authorised to use. - [x] v0.3 — parallel multi-agent (`std.conc`), VSCode extension, web frontend, bootstrap script - [x] v0.4 — lex-vcs tools (17), CLI one-shot mode, Ollama + vLLM providers, install target - [x] v0.5 — ACP server (`src/server/acp.lex`), ACP helpers in lex-agent +- [x] v0.6 — OpenCode Go provider (native + via the bundled LiteLLM proxy, shared config with lex-loom) + +--- + +## License + +EUPL-1.2 — matches the rest of the lex ecosystem. --- diff --git a/lex.toml b/lex.toml index ced9807..bc7e437 100644 --- a/lex.toml +++ b/lex.toml @@ -1,7 +1,9 @@ [package] -name = "lex-code" -version = "0.1.0" -lex = "0.9.7" +name = "lex-code" +version = "0.1.0" +lex = "0.10.10" +license = "EUPL-1.2" +description = "A Lex-native coding assistant — build/plan/explore/refactor/spec/test/review agents, TUI + A2A + ACP servers, lex-vcs tools." [dependencies] lex-llm = { git = "https://github.com/alpibrusl/lex-llm" } @@ -14,6 +16,7 @@ lex-web = { git = "https://github.com/alpibrusl/lex-web" } lex-orm = { git = "https://github.com/alpibrusl/lex-orm" } lex-mcp = { git = "https://github.com/alpibrusl/lex-mcp" } lex-agent-llm = { git = "https://github.com/alpibrusl/lex-agent-llm" } +lex-memory = { git = "https://github.com/alpibrusl/lex-memory" } [bin] name = "lex-code" diff --git a/litellm/config.yaml b/litellm/config.yaml new file mode 100644 index 0000000..91a4e12 --- /dev/null +++ b/litellm/config.yaml @@ -0,0 +1,302 @@ +# LiteLLM proxy config for lex-code — kept in sync with lex-loom's own +# litellm/config.yaml (same model list, including the OpenCode Go plan) +# since both repos are meant to work against the same proxy instance. +# +# Two auth modes: +# +# 1. API keys (Anthropic / OpenAI): +# Set ANTHROPIC_API_KEY and/or OPENAI_API_KEY in your environment before +# running docker compose, then use model names like claude-sonnet-4-6. +# +# 2. ChatGPT subscription (no API key needed): +# LiteLLM uses an OAuth device flow on first request — it prints a URL, +# you open it in a browser, and the token is cached in auth.json. +# Use model names prefixed with chatgpt/: chatgpt/gpt-5.3-chat-latest +# +# 3. Claude Code Max subscription: +# forward_client_headers_to_llm_api forwards the OAuth token that Claude +# Code places in the Authorization header. Set ANTHROPIC_BASE_URL=http://localhost:4000 +# in Claude Code's env to route it through this proxy for cost tracking. +# Note: lex-code itself still needs ANTHROPIC_API_KEY to talk to Claude. +# +# 4. OpenCode Go plan (see the "OpenCode Go plan" section below): +# Set OPENCODE_API_KEY before docker compose up, then point LITELLM_MODEL +# at any entry in that section (deepseek-v4-flash, qwen3.7-max, kimi-k3, ...). +# Prefer this over lex-code's native --opencode flag when you want a single +# proxy shared with lex-loom, request-timeout/retry policy, or cost tracking. +# +# Usage: +# cd litellm +# ANTHROPIC_API_KEY=... OPENAI_API_KEY=... OPENCODE_API_KEY=... docker compose up -d +# +# # lex-code via API key: +# LITELLM_BASE_URL=http://localhost:4000 LITELLM_MODEL=claude-sonnet-4-6 \ +# lex run --allow-effects env,io,net,llm,proc,sql,fs_write,time src/tui/main.lex main -- --litellm "implement list.zip" +# +# # lex-code via OpenCode Go through the proxy: +# LITELLM_BASE_URL=http://localhost:4000 LITELLM_MODEL=kimi-k2.7-code \ +# lex run --allow-effects env,io,net,llm,proc,sql,fs_write,time src/tui/main.lex main -- --litellm "implement list.zip" +# +# # lex-code via ChatGPT subscription: +# LITELLM_BASE_URL=http://localhost:4000 LITELLM_MODEL=chatgpt/gpt-5.3-chat-latest \ +# lex run --allow-effects env,io,net,llm,proc,sql,fs_write,time src/tui/main.lex main -- --litellm "implement list.zip" + +model_list: + + # ── Ollama local models (host machine) ──────────────────────────────────── + # Requires Ollama running on the host at port 11434. + # From Docker: api_base uses host.docker.internal (add extra_hosts in compose). + # From host: set OLLAMA_HOST=http://localhost:11434 or leave default. + # + # Recommended for code tasks: qwen3-coder:30b + # ollama pull qwen3-coder:30b + # LITELLM_BASE_URL=http://localhost:4000 MODEL=qwen3-coder:30b lex run src/main.lex + # + # NOTE on thinking models (gemma4:26b, deepseek-r1): + # These models spend 500-700 tokens on chain-of-thought BEFORE producing output. + # max_tokens must be >= 2000 or they return empty content. merge_reasoning_content_in_choices + # is required so the visible text is not silently dropped by LiteLLM. + # Tool calling with these models is unreliable under large context (10+ tools). + # Prefer qwen3-coder:30b or devstral-small-2 for code agent tasks. + + - model_name: qwen3-coder:30b + litellm_params: + model: ollama/qwen3-coder:30b + api_base: http://host.docker.internal:11434 + + - model_name: devstral-small-2:latest + litellm_params: + model: ollama/devstral-small-2:latest + api_base: http://host.docker.internal:11434 + + - model_name: gemma4:26b + litellm_params: + model: ollama/gemma4:26b + api_base: http://host.docker.internal:11434 + merge_reasoning_content_in_choices: true + + - model_name: gemma4:latest + litellm_params: + model: ollama/gemma4:latest + api_base: http://host.docker.internal:11434 + merge_reasoning_content_in_choices: true + + # ── Anthropic API key ────────────────────────────────────────────────────── + - model_name: claude-opus-4-7 + litellm_params: + model: anthropic/claude-opus-4-7 + api_key: os.environ/ANTHROPIC_API_KEY + + - model_name: claude-sonnet-4-6 + litellm_params: + model: anthropic/claude-sonnet-4-6 + api_key: os.environ/ANTHROPIC_API_KEY + + - model_name: claude-haiku-4-5 + litellm_params: + model: anthropic/claude-haiku-4-5-20251001 + api_key: os.environ/ANTHROPIC_API_KEY + + # ── OpenAI API key ───────────────────────────────────────────────────────── + - model_name: gpt-4o + litellm_params: + model: openai/gpt-4o + api_key: os.environ/OPENAI_API_KEY + + - model_name: gpt-4o-mini + litellm_params: + model: openai/gpt-4o-mini + api_key: os.environ/OPENAI_API_KEY + + - model_name: o3-mini + litellm_params: + model: openai/o3-mini + api_key: os.environ/OPENAI_API_KEY + + # ── ChatGPT subscription (device flow OAuth, no API key needed) ──────────── + # First request prints a browser URL to authenticate. Token is cached. + - model_name: chatgpt/gpt-5.3-chat-latest + litellm_params: + model: chatgpt/gpt-5.3-chat-latest + + - model_name: chatgpt/gpt-5.4 + litellm_params: + model: chatgpt/gpt-5.4 + + - model_name: chatgpt/gpt-5.3-codex + litellm_params: + model: chatgpt/gpt-5.3-codex + + # ── OpenCode Go plan ─────────────────────────────────────────────────────── + # Set OPENCODE_API_KEY=$(cat ~/.credentials/opencode/key) before compose up. + # All Go plan models run in thinking mode — merge_reasoning_content_in_choices + # is required on every entry so reasoning traces don't silently replace content. + # + # Note: tool_choice=required is rejected by all thinking-mode models. + # lex-code uses tool_choice=auto (via lex-llm, shared with lex-loom) so + # this works correctly. + + # DeepSeek + - model_name: deepseek-v4-flash + litellm_params: + model: openai/deepseek-v4-flash + api_base: https://opencode.ai/zen/go/v1 + api_key: os.environ/OPENCODE_API_KEY + merge_reasoning_content_in_choices: true + + - model_name: deepseek-v4-pro + litellm_params: + model: openai/deepseek-v4-pro + api_base: https://opencode.ai/zen/go/v1 + api_key: os.environ/OPENCODE_API_KEY + merge_reasoning_content_in_choices: true + + # Qwen3 (Alibaba) + - model_name: qwen3.7-max + litellm_params: + model: openai/qwen3.7-max + api_base: https://opencode.ai/zen/go/v1 + api_key: os.environ/OPENCODE_API_KEY + merge_reasoning_content_in_choices: true + + - model_name: qwen3.7-plus + litellm_params: + model: openai/qwen3.7-plus + api_base: https://opencode.ai/zen/go/v1 + api_key: os.environ/OPENCODE_API_KEY + merge_reasoning_content_in_choices: true + + - model_name: qwen3.6-plus + litellm_params: + model: openai/qwen3.6-plus + api_base: https://opencode.ai/zen/go/v1 + api_key: os.environ/OPENCODE_API_KEY + merge_reasoning_content_in_choices: true + + - model_name: qwen3.5-plus + litellm_params: + model: openai/qwen3.5-plus + api_base: https://opencode.ai/zen/go/v1 + api_key: os.environ/OPENCODE_API_KEY + merge_reasoning_content_in_choices: true + + # Kimi (Moonshot AI) + - model_name: kimi-k3 + litellm_params: + model: openai/kimi-k3 + api_base: https://opencode.ai/zen/go/v1 + api_key: os.environ/OPENCODE_API_KEY + merge_reasoning_content_in_choices: true + + - model_name: kimi-k2.7-code + litellm_params: + model: openai/kimi-k2.7-code + api_base: https://opencode.ai/zen/go/v1 + api_key: os.environ/OPENCODE_API_KEY + merge_reasoning_content_in_choices: true + + - model_name: kimi-k2.6 + litellm_params: + model: openai/kimi-k2.6 + api_base: https://opencode.ai/zen/go/v1 + api_key: os.environ/OPENCODE_API_KEY + merge_reasoning_content_in_choices: true + + - model_name: kimi-k2.5 + litellm_params: + model: openai/kimi-k2.5 + api_base: https://opencode.ai/zen/go/v1 + api_key: os.environ/OPENCODE_API_KEY + merge_reasoning_content_in_choices: true + + # MiniMax + - model_name: minimax-m3 + litellm_params: + model: openai/minimax-m3 + api_base: https://opencode.ai/zen/go/v1 + api_key: os.environ/OPENCODE_API_KEY + merge_reasoning_content_in_choices: true + + - model_name: minimax-m2.7 + litellm_params: + model: openai/minimax-m2.7 + api_base: https://opencode.ai/zen/go/v1 + api_key: os.environ/OPENCODE_API_KEY + merge_reasoning_content_in_choices: true + + - model_name: minimax-m2.5 + litellm_params: + model: openai/minimax-m2.5 + api_base: https://opencode.ai/zen/go/v1 + api_key: os.environ/OPENCODE_API_KEY + merge_reasoning_content_in_choices: true + + # GLM (Z.ai / Zhipu) + # glm-5.2 confirmed available on Go plan API (local models.json cache was stale) + - model_name: glm-5.2 + litellm_params: + model: openai/glm-5.2 + api_base: https://opencode.ai/zen/go/v1 + api_key: os.environ/OPENCODE_API_KEY + merge_reasoning_content_in_choices: true + + - model_name: glm-5.1 + litellm_params: + model: openai/glm-5.1 + api_base: https://opencode.ai/zen/go/v1 + api_key: os.environ/OPENCODE_API_KEY + merge_reasoning_content_in_choices: true + + - model_name: glm-5 + litellm_params: + model: openai/glm-5 + api_base: https://opencode.ai/zen/go/v1 + api_key: os.environ/OPENCODE_API_KEY + merge_reasoning_content_in_choices: true + + # MiMo (Xiaomi) + - model_name: mimo-v2.5-pro + litellm_params: + model: openai/mimo-v2.5-pro + api_base: https://opencode.ai/zen/go/v1 + api_key: os.environ/OPENCODE_API_KEY + merge_reasoning_content_in_choices: true + + - model_name: mimo-v2.5 + litellm_params: + model: openai/mimo-v2.5 + api_base: https://opencode.ai/zen/go/v1 + api_key: os.environ/OPENCODE_API_KEY + merge_reasoning_content_in_choices: true + + - model_name: mimo-v2-pro + litellm_params: + model: openai/mimo-v2-pro + api_base: https://opencode.ai/zen/go/v1 + api_key: os.environ/OPENCODE_API_KEY + merge_reasoning_content_in_choices: true + + - model_name: mimo-v2-omni + litellm_params: + model: openai/mimo-v2-omni + api_base: https://opencode.ai/zen/go/v1 + api_key: os.environ/OPENCODE_API_KEY + merge_reasoning_content_in_choices: true + + # ── Google Gemini API key (optional) ────────────────────────────────────── + # - model_name: gemini-2.5-flash + # litellm_params: + # model: gemini/gemini-2.5-flash + # api_key: os.environ/GOOGLE_API_KEY + +litellm_settings: + drop_params: true + request_timeout: 120 + num_retries: 2 + +general_settings: + # Enables Claude Code Max subscription forwarding. + # Claude Code must be configured with ANTHROPIC_BASE_URL=http://localhost:4000 + forward_client_headers_to_llm_api: true + # Uncomment to require a key on all requests: + # master_key: os.environ/LITELLM_API_KEY diff --git a/litellm/docker-compose.yml b/litellm/docker-compose.yml new file mode 100644 index 0000000..ecb75dc --- /dev/null +++ b/litellm/docker-compose.yml @@ -0,0 +1,23 @@ +services: + litellm: + image: ghcr.io/berriai/litellm:main-latest + restart: unless-stopped + ports: + - "4000:4000" + volumes: + - ./config.yaml:/app/config.yaml:ro + command: ["--config", "/app/config.yaml", "--port", "4000"] + environment: + ANTHROPIC_API_KEY: ${ANTHROPIC_API_KEY:-} + OPENAI_API_KEY: ${OPENAI_API_KEY:-} + GOOGLE_API_KEY: ${GOOGLE_API_KEY:-} + OPENCODE_API_KEY: ${OPENCODE_API_KEY:-} + LITELLM_API_KEY: ${LITELLM_API_KEY:-} + extra_hosts: + - "host.docker.internal:host-gateway" + healthcheck: + test: ["CMD", "curl", "-sf", "http://localhost:4000/health"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s diff --git a/src/agents/build.lex b/src/agents/build.lex index 7e36f33..104826c 100644 --- a/src/agents/build.lex +++ b/src/agents/build.lex @@ -51,6 +51,16 @@ fn google_agent() -> [env] ag.AgentLoop { ag.with_permission_gate(base, rules.build_permission()) } +# OpenCode Go plan (https://opencode.ai/docs/zen) — an OpenAI-compatible +# endpoint over full-scale cloud models (DeepSeek, Qwen3, Kimi, GLM, MiniMax, +# MiMo), not a small local model, so it gets the same full tool budget as the +# other cloud providers above rather than litellm/ollama's stripped-down +# treatment. OPENCODE_API_KEY required; OPENCODE_MODEL overrides the default. +fn opencode_agent() -> [env] ag.AgentLoop { + let base := { name: "build", goal: bpo.system(), model: prov.make_model_ref("opencode", tools.opencode_model()), provider: providers.opencode_go(), tools: tools.all_tools(), options: { temperature: None, top_p: None, max_steps: Some(50), max_tokens: None }, permission_spec: None } + ag.with_permission_gate(base, rules.build_permission()) +} + fn vertex_agent() -> [env] ag.AgentLoop { let base := { name: "build", goal: bpo.system(), model: vtx.gemini_35_flash(), provider: providers.vertex(), tools: tools.all_tools(), options: { temperature: None, top_p: None, max_steps: Some(50), max_tokens: None }, permission_spec: None } ag.with_permission_gate(base, rules.build_permission()) diff --git a/src/agents/explore.lex b/src/agents/explore.lex index ded5cd2..e0a1c03 100644 --- a/src/agents/explore.lex +++ b/src/agents/explore.lex @@ -45,3 +45,8 @@ fn google_agent() -> [env] ag.AgentLoop { ag.with_permission_gate(base, rules.explore_permission()) } +fn opencode_agent() -> [env] ag.AgentLoop { + let base := { name: "explore", goal: ep.system(), model: prov.make_model_ref("opencode", tools.opencode_model()), provider: providers.opencode_go(), tools: tools.tools_for_spec(rules.explore_permission()), options: { temperature: None, top_p: None, max_steps: Some(20), max_tokens: None }, permission_spec: None } + ag.with_permission_gate(base, rules.explore_permission()) +} + diff --git a/src/agents/plan.lex b/src/agents/plan.lex index da306b4..49717bb 100644 --- a/src/agents/plan.lex +++ b/src/agents/plan.lex @@ -45,3 +45,8 @@ fn google_agent() -> [env] ag.AgentLoop { ag.with_permission_gate(base, rules.plan_permission()) } +fn opencode_agent() -> [env] ag.AgentLoop { + let base := { name: "plan", goal: pp.system(), model: prov.make_model_ref("opencode", tools.opencode_model()), provider: providers.opencode_go(), tools: tools.tools_for_spec(rules.plan_permission()), options: { temperature: None, top_p: None, max_steps: Some(30), max_tokens: None }, permission_spec: None } + ag.with_permission_gate(base, rules.plan_permission()) +} + diff --git a/src/agents/refactor.lex b/src/agents/refactor.lex index f8109a7..d6f3d8c 100644 --- a/src/agents/refactor.lex +++ b/src/agents/refactor.lex @@ -45,3 +45,8 @@ fn google_agent() -> [env] ag.AgentLoop { ag.with_permission_gate(base, rules.refactor_permission()) } +fn opencode_agent() -> [env] ag.AgentLoop { + let base := { name: "refactor", goal: rp.system(), model: prov.make_model_ref("opencode", tools.opencode_model()), provider: providers.opencode_go(), tools: tools.tools_for_spec(rules.refactor_permission()), options: { temperature: None, top_p: None, max_steps: Some(40), max_tokens: None }, permission_spec: None } + ag.with_permission_gate(base, rules.refactor_permission()) +} + diff --git a/src/agents/review.lex b/src/agents/review.lex index 9b990bb..ac2941e 100644 --- a/src/agents/review.lex +++ b/src/agents/review.lex @@ -45,3 +45,8 @@ fn google_agent() -> [env] ag.AgentLoop { ag.with_permission_gate(base, rules.review_permission()) } +fn opencode_agent() -> [env] ag.AgentLoop { + let base := { name: "review", goal: rvp.system(), model: prov.make_model_ref("opencode", tools.opencode_model()), provider: providers.opencode_go(), tools: tools.tools_for_spec(rules.review_permission()), options: { temperature: None, top_p: None, max_steps: Some(25), max_tokens: None }, permission_spec: None } + ag.with_permission_gate(base, rules.review_permission()) +} + diff --git a/src/agents/spec_agent.lex b/src/agents/spec_agent.lex index b786974..89824f6 100644 --- a/src/agents/spec_agent.lex +++ b/src/agents/spec_agent.lex @@ -45,3 +45,8 @@ fn google_agent() -> [env] ag.AgentLoop { ag.with_permission_gate(base, rules.spec_permission()) } +fn opencode_agent() -> [env] ag.AgentLoop { + let base := { name: "spec", goal: sp.system(), model: prov.make_model_ref("opencode", tools.opencode_model()), provider: providers.opencode_go(), tools: tools.tools_for_spec(rules.spec_permission()), options: { temperature: None, top_p: None, max_steps: Some(30), max_tokens: None }, permission_spec: None } + ag.with_permission_gate(base, rules.spec_permission()) +} + diff --git a/src/agents/test_agent.lex b/src/agents/test_agent.lex index 0746cde..9cdb0ef 100644 --- a/src/agents/test_agent.lex +++ b/src/agents/test_agent.lex @@ -45,3 +45,8 @@ fn google_agent() -> [env] ag.AgentLoop { ag.with_permission_gate(base, rules.test_permission()) } +fn opencode_agent() -> [env] ag.AgentLoop { + let base := { name: "test", goal: tp.system(), model: prov.make_model_ref("opencode", tools.opencode_model()), provider: providers.opencode_go(), tools: tools.tools_for_spec(rules.test_permission()), options: { temperature: None, top_p: None, max_steps: Some(30), max_tokens: None }, permission_spec: None } + ag.with_permission_gate(base, rules.test_permission()) +} + diff --git a/src/project_memory.lex b/src/project_memory.lex index 65bf1c9..a431e3b 100644 --- a/src/project_memory.lex +++ b/src/project_memory.lex @@ -4,7 +4,8 @@ # separate from session logs so memory outlives individual sessions and # accumulates across every mode (Build, Explore, Refactor, Review, etc.). # -# Wraps lex-agent/src/memory with project-specific entry points: +# Wraps lex-memory/src/memory (extracted from lex-agent/src/memory, see +# alpibrusl/lex-memory#1) with project-specific entry points: # # convention — coding/style conventions, upserted by name # tech_stack — technology/framework facts, upserted by name @@ -22,7 +23,7 @@ # } # } -import "lex-agent/src/memory" as mem +import "lex-memory/src/memory" as mem import "lex-orm/src/connection" as conn diff --git a/src/server/session.lex b/src/server/session.lex index d677835..2840927 100644 --- a/src/server/session.lex +++ b/src/server/session.lex @@ -103,6 +103,15 @@ fn pick_agent(mode :: AgentMode, provider_tag :: Str) -> [env] ag.AgentLoop { Test => test_a.google_agent(), Review => review_a.google_agent(), }, + "opencode" => match mode { + Build => build_agent.opencode_agent(), + Plan => plan_agent.opencode_agent(), + Explore => explore_agent.opencode_agent(), + Refactor => refactor_agent.opencode_agent(), + Spec => spec_a.opencode_agent(), + Test => test_a.opencode_agent(), + Review => review_a.opencode_agent(), + }, _ => match mode { Build => build_agent.agent(), Plan => plan_agent.agent(), diff --git a/src/tools/index.lex b/src/tools/index.lex index 0bde9b5..d31d9a3 100644 --- a/src/tools/index.lex +++ b/src/tools/index.lex @@ -124,6 +124,18 @@ fn litellm_model() -> [env] Str { } } +# Model name for the native OpenCode Go provider (providers.opencode_go(), +# hits https://opencode.ai/zen/go/v1 directly — no local proxy needed). +# Default is a coding-oriented model on the Go plan; override with +# OPENCODE_MODEL. See litellm/config.yaml for the full Go-plan model list +# (also reachable via --litellm once the proxy is running). +fn opencode_model() -> [env] Str { + match env.get("OPENCODE_MODEL") { + None => "kimi-k2.7-code", + Some(m) => m, + } +} + # ── Dynamic toolset loading ───────────────────────────────────────────────── # Each gate is a lex-spec predicate that is `Allow` only when the matching # `loaded_*` binding is true. lex-llm sets those bindings by scanning the diff --git a/src/tui/main.lex b/src/tui/main.lex index f37a354..ac731e1 100644 --- a/src/tui/main.lex +++ b/src/tui/main.lex @@ -162,7 +162,11 @@ fn select_provider_tag(argv :: List[Str]) -> Str { if has_flag(argv, "--vllm") { "vllm" } else { - "anthropic" + if has_flag(argv, "--opencode") { + "opencode" + } else { + "anthropic" + } } } } @@ -244,7 +248,7 @@ fn main() -> [env, io, net, llm, proc, sql, fs_write, time] Nil { None => { io.print(str.concat("lex-code — Lex-specialized coding assistant", "\n")) io.print(str.concat("modes: --plan | --explore | --refactor | --spec | --test | --review | --multi", "\n")) - io.print(str.concat("providers: --mistral | --openai | --google | --vertex | --ollama | --vllm (default: anthropic)", "\n")) + io.print(str.concat("providers: --mistral | --openai | --google | --vertex | --litellm | --ollama | --vllm | --opencode (default: anthropic)", "\n")) io.print(str.concat("one-shot: lex run src/tui/main.lex -- [flags] \"your task\"", "\n")) io.print(str.concat("Ctrl-D to exit", "\n")) if has_flag(argv, "--multi") {