Skip to content

Commit 9c3d9c7

Browse files
JetoPistolaclaude
andcommitted
feat(test-local): default to keyless local Ollama provider; per-field Opik config
- Add a model-provider abstraction: `--provider ollama` (default) runs a local LLM in an Ollama sidecar (docker-compose.ollama.yml) so a real OpenClaw turn can be driven with no model-provider account or API key. `--provider openai` drives turns with a user-injected OPENAI_API_KEY (for stronger tool-calling or the Codex runtime that emits codex_app_server spans). - Address Baz review (entrypoint.sh): write plugins.entries.opik-openclaw.config via per-field `config set` dot-paths instead of a hand-built JSON5 blob, so quotes / backslashes in OPIK_* env vars are stored verbatim (confirmed: the blob form fails config validation on a `"` in the value). - Make OPIK_API_KEY optional (unauthenticated local Opik needs none). - Document Opik targets (Comet Cloud + local self-hosted via host.docker.internal). - Ollama config gotchas baked in: native baseUrl (no /v1), explicit models[] array, contextWindow large enough for the agent system prompt (default llama3.2:3b@131072). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 499b64a commit 9c3d9c7

4 files changed

Lines changed: 303 additions & 71 deletions

File tree

README.md

Lines changed: 50 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -237,47 +237,74 @@ the host.
237237

238238
Prerequisites:
239239

240-
- Docker
240+
- Docker (with Compose)
241241
- Node.js `>=22.12.0` and npm `>=10` (used on the host to pack the plugin)
242-
- An Opik project plus an `OPENAI_API_KEY` for the live model call
242+
- An Opik project to send traces to (see "Opik targets" below)
243243
- Optional: the GitHub `gh` CLI (used to check out PRs, including forks)
244244

245-
Set the required credentials in your shell or in a gitignored `.env` (copy from
246-
`.env.example`):
245+
### Model provider
246+
247+
By default the turn is driven by a **local LLM running in an Ollama sidecar** — no
248+
model-provider account or API key required. This is enough to verify trace export
249+
and LLM/tool spans. The first run pulls the model (default `llama3.2:3b`) into a
250+
named volume and reuses it afterwards; local CPU inference is slow.
251+
252+
To drive turns with a **real provider** instead (stronger tool-calling, or the Codex
253+
runtime that produces `codex_app_server` spans), pass `--provider openai` and supply
254+
your own `OPENAI_API_KEY`. The key is yours, injected at runtime only, never baked
255+
into the image.
256+
257+
### Opik targets
258+
259+
Point `OPIK_URL_OVERRIDE` at whichever Opik you want traces to land in:
260+
261+
- **Comet Cloud:** `https://www.comet.com/opik/api` (set `OPIK_API_KEY`)
262+
- **Local self-hosted Opik on your host:** `http://host.docker.internal:5173/api`
263+
(`OPIK_API_KEY` can be omitted for unauthenticated local deployments)
264+
265+
Set config in your shell or in a gitignored `.env` (copy from `.env.example`):
247266

248267
```bash
249-
export OPIK_API_KEY="your-api-key"
250268
export OPIK_URL_OVERRIDE="https://www.comet.com/opik/api"
251-
export OPENAI_API_KEY="sk-..."
269+
export OPIK_API_KEY="your-api-key" # optional for unauthenticated local Opik
252270
# optional: OPIK_PROJECT_NAME (default openclaw), OPIK_WORKSPACE (default default)
271+
# only for --provider openai:
272+
export OPENAI_API_KEY="sk-..."
253273
```
254274

255275
Run a PR, a branch, or the current working tree:
256276

257277
```bash
258-
./scripts/test-local.sh 114 # check out PR #114 and test it
259-
./scripts/test-local.sh my-feature-branch # test a branch
260-
./scripts/test-local.sh --current # test the current working tree as-is
278+
./scripts/test-local.sh 114 # PR #114, local Ollama (default)
279+
./scripts/test-local.sh my-feature-branch # a branch
280+
./scripts/test-local.sh --current # the current working tree as-is
281+
./scripts/test-local.sh 114 --provider openai # drive with a real provider / Codex
261282
```
262283

263284
The host runs only git: it checks out the PR/branch and exports a source snapshot
264285
with `git archive` (which executes no project code). The snapshot is mounted
265286
read-only into a clean Node 22 image that runs the latest `openclaw` (pin a
266-
specific version with `OPENCLAW_VERSION`); the container does `npm ci && npm pack`, installs the build,
267-
starts the gateway, and drops you into a shell. List the configured agents, then
268-
run a turn that triggers tool calls:
287+
specific version with `OPENCLAW_VERSION`); the container does `npm ci && npm pack`,
288+
installs the build, configures the provider + Opik export, starts the gateway, and
289+
drops you into a shell. List the configured agents, then run a turn that triggers
290+
tool calls:
269291

270292
```bash
271293
openclaw agents list
272-
openclaw agent --agent <id> --message "Use the shell tool to run 'echo hello', then reply done."
273-
# or run the embedded agent without routing:
274-
openclaw agent --local --message "Use the shell tool to run 'echo hello', then reply done."
294+
openclaw agent --session-id test1 --message "Use the shell tool to run 'echo hello', then reply done."
275295
```
276296

277297
Then open your Opik project and confirm the trace shows an LLM span and a tool span
278298
(e.g. `shell` / `apply_patch`), with `metadata.created_from = "openclaw"`. For
279-
Codex-native runs (PR #114) the spans also carry `metadata.source = "codex_app_server"`.
280-
Type `exit` to tear the container down.
299+
Codex-native runs (PR #114, `--provider openai`) the spans also carry
300+
`metadata.source = "codex_app_server"`. Type `exit` to tear the container down.
301+
302+
Notes:
303+
304+
- Small local models call tools less reliably than hosted ones; if a turn answers in
305+
text instead of calling the tool, retry or use `--provider openai`.
306+
- Override the local model with `OLLAMA_MODEL` (must be tool-capable, e.g.
307+
`qwen2.5-coder:7b`).
281308

282309
### Safety model
283310

@@ -294,11 +321,12 @@ is not a substitute for trusting the code you run.
294321
`tmpfs` only), and pid/memory caps. The source snapshot is mounted read-only.
295322
- **Disposable.** `--rm` deletes the container and its writable layer on exit.
296323
- **Secrets at runtime only.** Credentials come from the environment (or a gitignored
297-
`.env`) via `docker run -e`; they are never baked into the image. Note they are
298-
live inside the running container by design, so treat `OPIK_API_KEY` /
299-
`OPENAI_API_KEY` as scoped, burnable keys rather than shared production secrets.
300-
- **Not a sandbox.** Plain Docker shares the host kernel and has open network egress
301-
(needed to reach Opik/OpenAI). Do not use this to run PRs you do not trust.
324+
`.env`); they are never baked into the image. Any key you pass (`OPIK_API_KEY`, or
325+
`OPENAI_API_KEY` with `--provider openai`) is live inside the running container by
326+
design, so treat it as scoped/burnable rather than a shared production secret. The
327+
default Ollama provider needs no key at all.
328+
- **Not a sandbox.** Plain Docker shares the host kernel and has open network egress.
329+
Do not use this to run PRs you do not trust.
302330

303331
## Contributing
304332

docker/docker-compose.ollama.yml

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# Ollama-backed local E2E: a local LLM sidecar so a real OpenClaw turn can be driven
2+
# with no model-provider account or API key. test-local.sh sets the env vars below.
3+
#
4+
# SRC_DIR host path to the git-archive source snapshot (mounted read-only)
5+
# OUT_DIR host path for run artifacts / span results
6+
# OLLAMA_MODEL tool-capable model to pull and run (default llama3.2:3b)
7+
#
8+
# The tester keeps the same hardening as the plain `docker run` path; the ollama
9+
# service holds the model in a named volume so it is pulled once and reused.
10+
services:
11+
ollama:
12+
image: ollama/ollama
13+
environment:
14+
OLLAMA_KEEP_ALIVE: "30m"
15+
volumes:
16+
- ollama-models:/root/.ollama
17+
networks:
18+
- e2e
19+
healthcheck:
20+
test: ["CMD", "ollama", "list"]
21+
interval: 5s
22+
timeout: 5s
23+
retries: 12
24+
25+
tester:
26+
image: opik-openclaw-e2e:local
27+
depends_on:
28+
ollama:
29+
condition: service_healthy
30+
environment:
31+
MODEL_PROVIDER: ollama
32+
OLLAMA_BASE_URL: "http://ollama:11434"
33+
OLLAMA_MODEL: "${OLLAMA_MODEL:-llama3.2:3b}"
34+
OPIK_API_KEY: "${OPIK_API_KEY}"
35+
OPIK_URL_OVERRIDE: "${OPIK_URL_OVERRIDE}"
36+
OPIK_PROJECT_NAME: "${OPIK_PROJECT_NAME:-openclaw}"
37+
OPIK_WORKSPACE: "${OPIK_WORKSPACE:-default}"
38+
volumes:
39+
- "${SRC_DIR}:/src:ro"
40+
- "${OUT_DIR}:/out"
41+
networks:
42+
- e2e
43+
# Lets OPIK_URL_OVERRIDE point at a host-run local Opik via host.docker.internal.
44+
extra_hosts:
45+
- "host.docker.internal:host-gateway"
46+
stdin_open: true
47+
tty: true
48+
# Hardening (mirrors scripts/test-local.sh): unprivileged, no new privileges,
49+
# read-only rootfs with writable tmpfs only, bounded blast radius.
50+
cap_drop:
51+
- ALL
52+
security_opt:
53+
- no-new-privileges
54+
read_only: true
55+
pids_limit: 512
56+
mem_limit: 4g
57+
tmpfs:
58+
- /work:exec,size=2g,uid=1000,gid=1000
59+
- /home/node:exec,size=3g,uid=1000,gid=1000
60+
- /tmp:size=512m,uid=1000,gid=1000
61+
62+
networks:
63+
e2e:
64+
driver: bridge
65+
66+
volumes:
67+
ollama-models:

docker/entrypoint.sh

Lines changed: 75 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,35 @@ if [[ ! -d "${SRC_DIR}" || ! -f "${SRC_DIR}/package.json" ]]; then
2020
exit 1
2121
fi
2222

23-
: "${OPIK_API_KEY:?OPIK_API_KEY is required}"
2423
: "${OPIK_URL_OVERRIDE:?OPIK_URL_OVERRIDE is required (e.g. https://www.comet.com/opik/api)}"
25-
: "${OPENAI_API_KEY:?OPENAI_API_KEY is required for the live model call}"
24+
# Optional: unauthenticated local Opik deployments do not need a key.
25+
OPIK_API_KEY="${OPIK_API_KEY:-}"
2626
OPIK_PROJECT_NAME="${OPIK_PROJECT_NAME:-openclaw}"
2727
OPIK_WORKSPACE="${OPIK_WORKSPACE:-default}"
28-
LIVE_MODEL="${OPENCLAW_LIVE_MODEL:-gpt-4o-mini}"
2928
GATEWAY_PORT="${OPENCLAW_GATEWAY_PORT:-18789}"
3029
GATEWAY_TOKEN="${OPENCLAW_GATEWAY_TOKEN:-local-e2e-token}"
3130

31+
# Model provider. Default is a local Ollama sidecar (no account, no real key).
32+
# Set MODEL_PROVIDER=openai (and OPENAI_API_KEY) to drive turns with a real
33+
# provider instead — e.g. to exercise the Codex runtime or a stronger model.
34+
MODEL_PROVIDER="${MODEL_PROVIDER:-ollama}"
35+
if [[ "${MODEL_PROVIDER}" == "ollama" ]]; then
36+
OLLAMA_BASE_URL="${OLLAMA_BASE_URL:-http://ollama:11434}"
37+
OLLAMA_MODEL="${OLLAMA_MODEL:-llama3.2:3b}"
38+
# Declare the model's full context. OpenClaw blocks windows below 16000 AND sizes
39+
# its prompt budget from this number, so it must be large enough to hold the agent
40+
# system prompt — too small overflows regardless of model size.
41+
OLLAMA_CONTEXT_WINDOW="${OLLAMA_CONTEXT_WINDOW:-131072}"
42+
AGENT_MODEL="ollama/${OLLAMA_MODEL}"
43+
elif [[ "${MODEL_PROVIDER}" == "openai" ]]; then
44+
: "${OPENAI_API_KEY:?OPENAI_API_KEY is required when MODEL_PROVIDER=openai}"
45+
LIVE_MODEL="${OPENCLAW_LIVE_MODEL:-gpt-4o-mini}"
46+
AGENT_MODEL="openai/${LIVE_MODEL}"
47+
else
48+
err "unsupported MODEL_PROVIDER: ${MODEL_PROVIDER} (expected 'ollama' or 'openai')"
49+
exit 1
50+
fi
51+
3252
# GATEWAY_PORT is interpolated unquoted into the config JSON below; a non-integer
3353
# would produce invalid JSON and fail the gateway with an opaque error, so reject it now.
3454
if ! [[ "${GATEWAY_PORT}" =~ ^[0-9]+$ ]]; then
@@ -66,6 +86,39 @@ export OPENCLAW_GATEWAY_TOKEN="${GATEWAY_TOKEN}"
6686
# Write only the gateway + model defaults first. The plugin entry must NOT exist
6787
# yet: `openclaw plugins install` validates config and aborts if it references a
6888
# not-yet-installed plugin, and it injects plugins.entries.opik-openclaw itself.
89+
#
90+
# For Ollama, declare an explicit provider (baseUrl points at the sidecar; explicit
91+
# config disables auto-discovery, so a manual model entry is required). Use the
92+
# native Ollama API URL — no /v1 suffix, which would break tool calling.
93+
if [[ "${MODEL_PROVIDER}" == "ollama" ]]; then
94+
PROVIDERS_BLOCK=$(cat <<JSON
95+
,
96+
"models": {
97+
"providers": {
98+
"ollama": {
99+
"apiKey": "ollama-local",
100+
"baseUrl": "${OLLAMA_BASE_URL}",
101+
"api": "ollama",
102+
"models": [
103+
{
104+
"id": "${OLLAMA_MODEL}",
105+
"name": "${OLLAMA_MODEL}",
106+
"reasoning": false,
107+
"input": ["text"],
108+
"cost": { "input": 0, "output": 0, "cacheRead": 0, "cacheWrite": 0 },
109+
"contextWindow": ${OLLAMA_CONTEXT_WINDOW},
110+
"maxTokens": 2048
111+
}
112+
]
113+
}
114+
}
115+
}
116+
JSON
117+
)
118+
else
119+
PROVIDERS_BLOCK=""
120+
fi
121+
69122
cat > "${CONFIG_PATH}" <<JSON
70123
{
71124
"gateway": {
@@ -76,22 +129,31 @@ cat > "${CONFIG_PATH}" <<JSON
76129
},
77130
"agents": {
78131
"defaults": {
79-
"model": { "primary": "openai/${LIVE_MODEL}" }
132+
"model": { "primary": "${AGENT_MODEL}" }
80133
}
81-
}
134+
}${PROVIDERS_BLOCK}
82135
}
83136
JSON
84137

85138
info "installing plugin build into OpenClaw..."
86139
${OPENCLAW} plugins install "${TARBALL_PATH}"
87140

88-
# Merge Opik settings into the install-updated config via config set (dot-path,
89-
# JSON5 value) rather than overwriting the file — install expands many defaults we
90-
# must preserve. No "hooks" key: openclaw 2026.3.2 rejects it as unrecognized; the
91-
# plugin registers its own conversation hooks on load.
141+
# Merge Opik settings into the install-updated config via config set, one field at a
142+
# time. Setting each scalar with its own dot-path passes the value as a raw string, so
143+
# quotes/backslashes in OPIK_* env vars are stored verbatim — a hand-built JSON5 blob
144+
# would break parsing or corrupt the value. install expands many defaults we preserve.
145+
# No "hooks" key: openclaw 2026.3.2 rejects it as unrecognized; the plugin registers
146+
# its own conversation hooks on load. tags is a fixed literal (no user input).
92147
info "configuring Opik export..."
93-
${OPENCLAW} config set plugins.entries.opik-openclaw.config \
94-
"{enabled:true,apiUrl:\"${OPIK_URL_OVERRIDE}\",apiKey:\"${OPIK_API_KEY}\",projectName:\"${OPIK_PROJECT_NAME}\",workspaceName:\"${OPIK_WORKSPACE}\",tags:[\"local-docker-e2e\"]}"
148+
PLUGIN_CFG="plugins.entries.opik-openclaw.config"
149+
${OPENCLAW} config set "${PLUGIN_CFG}.enabled" true
150+
${OPENCLAW} config set "${PLUGIN_CFG}.apiUrl" "${OPIK_URL_OVERRIDE}"
151+
if [[ -n "${OPIK_API_KEY}" ]]; then
152+
${OPENCLAW} config set "${PLUGIN_CFG}.apiKey" "${OPIK_API_KEY}"
153+
fi
154+
${OPENCLAW} config set "${PLUGIN_CFG}.projectName" "${OPIK_PROJECT_NAME}"
155+
${OPENCLAW} config set "${PLUGIN_CFG}.workspaceName" "${OPIK_WORKSPACE}"
156+
${OPENCLAW} config set "${PLUGIN_CFG}.tags" '["local-docker-e2e"]'
95157
${OPENCLAW} config set plugins.allow '["opik-openclaw"]'
96158

97159
if ! ${OPENCLAW} config validate; then
@@ -130,6 +192,8 @@ cat <<GUIDE
130192
131193
$(info "gateway ready — plugin installed and tracing to Opik")
132194
195+
Provider: ${MODEL_PROVIDER}
196+
Model: ${AGENT_MODEL}
133197
Project: ${OPIK_PROJECT_NAME}
134198
Workspace: ${OPIK_WORKSPACE}
135199
Endpoint: ${OPIK_URL_OVERRIDE}

0 commit comments

Comments
 (0)