Initial commit: localgenai stack
Containerized local LLM stack for the Framework Desktop / Strix Halo,
plus the OpenCode harness on the Mac side.
- pyinfra/framework/: pyinfra deploy targeting the box
- llama.cpp (Vulkan), vLLM (ROCm), Ollama (ROCm with HSA override
for gfx1151), OpenWebUI
- Beszel (host + container + AMD GPU dashboard via sysfs)
- OpenLIT (LLM fleet metrics)
- Phoenix (per-trace agent waterfall)
- OpenHands (autonomous agent in a Docker sandbox)
- opencode/: OpenCode config + Phoenix bridge plugin (OTel exporter)
- install.sh deploys to ~/.config/opencode/
- StrixHaloSetup.md / StrixHaloMemory.md / Roadmap.md / TODO.md:
documentation and planning
- testing/qwen3-coder-30b/: small evaluation harness
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
7
opencode/.gitignore
vendored
Normal file
7
opencode/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# Plugin deps — install.sh's `npm install` repopulates these.
|
||||
.opencode/plugin/node_modules/
|
||||
|
||||
# Diagnostic log written by phoenix-bridge.js (Mac path).
|
||||
/tmp/phoenix-bridge.log
|
||||
|
||||
.DS_Store
|
||||
1685
opencode/.opencode/plugin/package-lock.json
generated
Normal file
1685
opencode/.opencode/plugin/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
13
opencode/.opencode/plugin/package.json
Normal file
13
opencode/.opencode/plugin/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "localgenai-opencode-plugins",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"description": "OpenCode plugins for the localgenai stack. Run `npm install` here once.",
|
||||
"dependencies": {
|
||||
"@opentelemetry/api": "^1.9.0",
|
||||
"@opentelemetry/exporter-trace-otlp-proto": "^0.205.0",
|
||||
"@opentelemetry/sdk-node": "^0.205.0",
|
||||
"@opentelemetry/sdk-trace-base": "^2.0.0"
|
||||
}
|
||||
}
|
||||
198
opencode/.opencode/plugin/phoenix-bridge.js
Normal file
198
opencode/.opencode/plugin/phoenix-bridge.js
Normal file
@@ -0,0 +1,198 @@
|
||||
// phoenix-bridge — register an OpenTelemetry SDK so the spans OpenCode
|
||||
// already emits (when experimental.openTelemetry=true) actually leave
|
||||
// the process and land in Arize Phoenix on the Framework Desktop.
|
||||
//
|
||||
// What it does:
|
||||
// - Boots @opentelemetry/sdk-node with OTLP/HTTP exporter pointed at
|
||||
// http://framework:4318/v1/traces (override via PHOENIX_OTLP_ENDPOINT).
|
||||
// - Inserts @arizeai/openinference-vercel's OpenInferenceSpanProcessor so
|
||||
// Phoenix's LLM/tool-aware UI gets richer attributes than vanilla OTel.
|
||||
// - Opens a parent span around each OpenCode session, with sub-sessions
|
||||
// (subagents spawned via the Task tool) nested under the parent. This
|
||||
// gives Phoenix a unified tree per top-level conversation.
|
||||
//
|
||||
// Caveats (May 2026):
|
||||
// - Subagent OTel context propagation across session boundaries is best
|
||||
// effort; OpenCode's experimental.chat.system.transform doesn't expose
|
||||
// sessionID yet (issue sst/opencode#6142). When that lands, swap the
|
||||
// manual stitching here for proper traceparent injection.
|
||||
// - Dynamic imports below are deliberate — per OpenCode's plugin guide,
|
||||
// direct imports of optional deps freeze the harness if they're absent.
|
||||
// Run `npm install` in this directory first.
|
||||
|
||||
export const PhoenixBridge = async ({ project, directory, worktree }) => {
|
||||
// Direct file logging — OpenCode swallows stdout/stderr from plugin
|
||||
// code, so this is how we get visibility into init progress. Tail with:
|
||||
// tail -f /tmp/phoenix-bridge.log
|
||||
const { appendFileSync } = await import("node:fs");
|
||||
const log = (msg) => {
|
||||
try {
|
||||
appendFileSync(
|
||||
"/tmp/phoenix-bridge.log",
|
||||
`[${new Date().toISOString()}] ${msg}\n`,
|
||||
);
|
||||
} catch (_) {
|
||||
/* best effort */
|
||||
}
|
||||
};
|
||||
log("plugin function entered");
|
||||
|
||||
// Phoenix 15.x serves OTLP/HTTP at /v1/traces on the same port as the UI
|
||||
// (6006). Earlier versions used a separate 4318 — override here if you
|
||||
// ever pin Phoenix < 15.0.
|
||||
const endpoint =
|
||||
process.env.PHOENIX_OTLP_ENDPOINT || "http://framework:6006/v1/traces";
|
||||
const serviceName = process.env.PHOENIX_SERVICE_NAME || "opencode";
|
||||
// NodeSDK's `serviceName` constructor option is ignored in some
|
||||
// versions; setting OTEL_SERVICE_NAME forces the resource attribute
|
||||
// through the standard env-var path. Has to happen before sdk.start().
|
||||
if (!process.env.OTEL_SERVICE_NAME) {
|
||||
process.env.OTEL_SERVICE_NAME = serviceName;
|
||||
}
|
||||
log(`endpoint=${endpoint} serviceName=${serviceName}`);
|
||||
|
||||
// Dynamic imports so a missing dep produces a warning, not a freeze.
|
||||
let NodeSDK,
|
||||
OTLPTraceExporter,
|
||||
BatchSpanProcessor,
|
||||
SimpleSpanProcessor,
|
||||
trace,
|
||||
context,
|
||||
diag,
|
||||
DiagLogLevel;
|
||||
try {
|
||||
({ NodeSDK } = await import("@opentelemetry/sdk-node"));
|
||||
// Use the protobuf-over-HTTP exporter — Phoenix's OTLP receiver
|
||||
// only speaks protobuf (returns 415 for the JSON variant from
|
||||
// @opentelemetry/exporter-trace-otlp-http).
|
||||
({ OTLPTraceExporter } = await import(
|
||||
"@opentelemetry/exporter-trace-otlp-proto"
|
||||
));
|
||||
({ BatchSpanProcessor, SimpleSpanProcessor } = await import(
|
||||
"@opentelemetry/sdk-trace-base"
|
||||
));
|
||||
({ trace, context, diag, DiagLogLevel } = await import(
|
||||
"@opentelemetry/api"
|
||||
));
|
||||
log("OTel imports resolved");
|
||||
} catch (err) {
|
||||
console.warn(
|
||||
"[phoenix-bridge] OTel deps not installed — skipping. " +
|
||||
"Run `npm install` in .opencode/plugin/ to enable.",
|
||||
err && err.message,
|
||||
);
|
||||
return {};
|
||||
}
|
||||
|
||||
// Pipe OTel's internal diagnostics into our log so export errors are
|
||||
// visible. Without this, BatchSpanProcessor swallows exporter failures.
|
||||
// Default to WARN — bump to DEBUG via PHOENIX_OTEL_DEBUG=1 when chasing
|
||||
// a regression.
|
||||
const otelLevel =
|
||||
process.env.PHOENIX_OTEL_DEBUG === "1"
|
||||
? DiagLogLevel.DEBUG
|
||||
: DiagLogLevel.WARN;
|
||||
diag.setLogger(
|
||||
{
|
||||
verbose: (m) => log(`[otel:verbose] ${m}`),
|
||||
debug: (m) => log(`[otel:debug] ${m}`),
|
||||
info: (m) => log(`[otel:info] ${m}`),
|
||||
warn: (m) => log(`[otel:warn] ${m}`),
|
||||
error: (m) => log(`[otel:error] ${m}`),
|
||||
},
|
||||
otelLevel,
|
||||
);
|
||||
|
||||
// NodeSDK accepts `serviceName` directly, sidestepping the Resource API
|
||||
// (which broke between @opentelemetry/resources v1.x and v2.x).
|
||||
// SimpleSpanProcessor (vs BatchSpanProcessor) exports each span
|
||||
// immediately — easier to debug while we sort out the pipeline.
|
||||
const exporter = new OTLPTraceExporter({ url: endpoint });
|
||||
const sdk = new NodeSDK({
|
||||
serviceName,
|
||||
spanProcessors: [new SimpleSpanProcessor(exporter)],
|
||||
});
|
||||
sdk.start();
|
||||
log("sdk.start() returned");
|
||||
|
||||
const tracer = trace.getTracer("opencode.phoenix-bridge");
|
||||
log("tracer obtained");
|
||||
|
||||
// Smoke-test span. If this one doesn't show up in Phoenix, the export
|
||||
// path is broken and there's no point chasing missing session spans.
|
||||
// Should appear under service "opencode" within ~5 s of OpenCode start.
|
||||
const bootSpan = tracer.startSpan("phoenix-bridge.boot", {
|
||||
attributes: {
|
||||
"opencode.endpoint": endpoint,
|
||||
"opencode.service_name": serviceName,
|
||||
},
|
||||
});
|
||||
bootSpan.end();
|
||||
log("boot span emitted (will flush within ~5s)");
|
||||
// sessionId -> { span, ctx }
|
||||
const sessions = new Map();
|
||||
|
||||
const closeSession = (sessionId) => {
|
||||
const rec = sessions.get(sessionId);
|
||||
if (!rec) return;
|
||||
rec.span.end();
|
||||
sessions.delete(sessionId);
|
||||
};
|
||||
|
||||
const onShutdown = async () => {
|
||||
for (const id of [...sessions.keys()]) closeSession(id);
|
||||
try {
|
||||
await sdk.shutdown();
|
||||
} catch (_) {
|
||||
/* ignore */
|
||||
}
|
||||
};
|
||||
process.once("beforeExit", onShutdown);
|
||||
process.once("SIGINT", onShutdown);
|
||||
process.once("SIGTERM", onShutdown);
|
||||
|
||||
return {
|
||||
event: async ({ event }) => {
|
||||
if (!event || !event.type) return;
|
||||
|
||||
switch (event.type) {
|
||||
case "session.created": {
|
||||
const session = event.properties?.session || event.properties || {};
|
||||
const id = session.id;
|
||||
const parentID = session.parentID;
|
||||
if (!id) return;
|
||||
|
||||
const parentRec = parentID ? sessions.get(parentID) : null;
|
||||
const parentCtx = parentRec ? parentRec.ctx : context.active();
|
||||
const span = tracer.startSpan(
|
||||
parentID ? `subagent ${id}` : `session ${id}`,
|
||||
{
|
||||
attributes: {
|
||||
"opencode.session.id": id,
|
||||
"opencode.session.parent_id": parentID || "",
|
||||
"opencode.directory": directory || "",
|
||||
"opencode.worktree": worktree || "",
|
||||
"opencode.project": project?.name || "",
|
||||
},
|
||||
},
|
||||
parentCtx,
|
||||
);
|
||||
sessions.set(id, { span, ctx: trace.setSpan(parentCtx, span) });
|
||||
break;
|
||||
}
|
||||
|
||||
case "session.idle":
|
||||
case "session.deleted":
|
||||
case "session.compacted": {
|
||||
const id =
|
||||
event.properties?.session?.id || event.properties?.id || null;
|
||||
if (id) closeSession(id);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
138
opencode/README.md
Normal file
138
opencode/README.md
Normal file
@@ -0,0 +1,138 @@
|
||||
# opencode setup
|
||||
|
||||
Canonical OpenCode config + Phoenix bridge plugin for the localgenai
|
||||
stack. `install.sh` deploys it to `~/.config/opencode/` on a Mac.
|
||||
|
||||
## What's wired up
|
||||
|
||||
- **Local model**: `framework/qwen3-coder:30b` served by Ollama on the
|
||||
Framework Desktop, reachable over Tailscale.
|
||||
- **Playwright MCP** ([@playwright/mcp](https://github.com/microsoft/playwright-mcp)) —
|
||||
browser automation. The model can navigate pages, click, fill forms,
|
||||
read DOM snapshots. Closes the agentic-browsing gap.
|
||||
- **SearXNG MCP** ([mcp-searxng](https://github.com/ihor-sokoliuk/mcp-searxng)) —
|
||||
web search via your self-hosted instance at <https://searxng.n0n.io>.
|
||||
No external API keys, no rate-limit roulette.
|
||||
- **Phoenix bridge plugin** (`.opencode/plugin/phoenix-bridge.js`) —
|
||||
exports OpenTelemetry spans for every LLM call, tool call, and
|
||||
subagent invocation to the Phoenix container running on the Framework
|
||||
Desktop. Per-prompt waterfall / flamegraph viz at
|
||||
<http://framework:6006>.
|
||||
|
||||
## Setup
|
||||
|
||||
```sh
|
||||
./install.sh
|
||||
```
|
||||
|
||||
Idempotent — re-run after editing `opencode.json` or pulling changes to
|
||||
the plugin. Each step checks before doing work. Specifically:
|
||||
|
||||
1. Verifies Homebrew is present (won't install it for you)
|
||||
2. `brew install node uv jq sst/tap/opencode` (skips if already at latest)
|
||||
3. Pre-caches Playwright's chromium so the first MCP call is instant
|
||||
4. `npm install` in `.opencode/plugin/` for the Phoenix bridge OTel deps
|
||||
5. Generates `~/.config/opencode/opencode.json` from the repo's
|
||||
`opencode.json`, rewriting relative plugin paths to absolute so
|
||||
OpenCode loads the plugin regardless of which directory it's launched
|
||||
from
|
||||
|
||||
Step 5 is the reason the deployed config isn't a plain symlink. The
|
||||
repo's `opencode.json` uses a relative plugin path (`./...`) so it stays
|
||||
valid in place; the deployed copy is generated with that path resolved
|
||||
to an absolute one. Edits to the repo's `opencode.json` need a re-run
|
||||
of `./install.sh` to take effect.
|
||||
|
||||
## Verify
|
||||
|
||||
```sh
|
||||
# Local model reachable
|
||||
curl -s http://framework:11434/v1/models | jq '.data[].id'
|
||||
|
||||
# SearXNG instance answers JSON
|
||||
curl -s 'https://searxng.n0n.io/search?q=test&format=json' | jq '.results | length'
|
||||
```
|
||||
|
||||
Then in opencode:
|
||||
|
||||
```
|
||||
opencode
|
||||
> /mcp # should list playwright and searxng as connected
|
||||
> search the web for "qwen3-coder benchmarks"
|
||||
> open https://example.com and tell me the H1
|
||||
```
|
||||
|
||||
## Phoenix tracing
|
||||
|
||||
The plugin at `.opencode/plugin/phoenix-bridge.js` boots an OpenTelemetry
|
||||
SDK on OpenCode startup and ships every span to Phoenix on the Framework
|
||||
Desktop. With `experimental.openTelemetry: true` (already set in
|
||||
`opencode.json`), OpenCode emits Vercel AI SDK spans that Phoenix renders
|
||||
as a per-turn waterfall: user prompt → main agent's `ai.streamText` →
|
||||
each tool call (built-in + MCP) with token counts and latencies inline.
|
||||
|
||||
The plugin uses `@opentelemetry/exporter-trace-otlp-proto` (not `-http`)
|
||||
because Phoenix's OTLP receiver only speaks protobuf — the JSON variant
|
||||
returns 415.
|
||||
|
||||
Defaults can be overridden via env vars (set before launching opencode):
|
||||
|
||||
| Variable | Default | Purpose |
|
||||
|---|---|---|
|
||||
| `PHOENIX_OTLP_ENDPOINT` | `http://framework:6006/v1/traces` | OTLP/HTTP target |
|
||||
| `PHOENIX_SERVICE_NAME` | `opencode` | Phoenix project name |
|
||||
| `PHOENIX_OTEL_DEBUG` | unset | `1` to surface OTel internal logs |
|
||||
|
||||
### Verifying
|
||||
|
||||
```sh
|
||||
: > /tmp/phoenix-bridge.log # truncate prior runs
|
||||
opencode # any directory; CWD doesn't matter
|
||||
tail -f /tmp/phoenix-bridge.log
|
||||
```
|
||||
|
||||
Healthy startup looks like:
|
||||
```
|
||||
plugin function entered
|
||||
endpoint=http://framework:6006/v1/traces serviceName=opencode
|
||||
OTel imports resolved
|
||||
sdk.start() returned
|
||||
tracer obtained
|
||||
boot span emitted (will flush within ~5s)
|
||||
```
|
||||
|
||||
Then open <http://framework:6006/projects> — an `opencode` project should
|
||||
appear with at least one `phoenix-bridge.boot` span. Send a prompt in
|
||||
OpenCode and real LLM-call traces follow.
|
||||
|
||||
If the plugin's deps aren't installed, OpenCode logs a warning and the
|
||||
plugin no-ops — the rest of OpenCode still works fine.
|
||||
|
||||
### Known limitations
|
||||
|
||||
- **Subagent nesting is best-effort.** The plugin opens a parent span
|
||||
per session and tries to stitch child sessions (Task-tool subagents)
|
||||
under their parent, but Vercel AI SDK spans live in their own OTel
|
||||
trace context. Until [sst/opencode#6142](https://github.com/sst/opencode/issues/6142)
|
||||
exposes `sessionID` in the `chat.system.transform` hook, child-session
|
||||
spans may show as separate traces in Phoenix.
|
||||
- **Console output from plugins is swallowed by OpenCode's TUI.** That's
|
||||
why init progress goes to `/tmp/phoenix-bridge.log` rather than stdout.
|
||||
|
||||
## Notes
|
||||
|
||||
- **SearXNG JSON output** must be enabled on the instance for the MCP
|
||||
server to work. If `format=json` returns HTML or 403, edit
|
||||
`settings.yml` on the SearXNG box: `search.formats: [html, json]`,
|
||||
restart.
|
||||
- **Playwright first-run** downloads ~200 MB of browser binaries into
|
||||
`~/Library/Caches/ms-playwright/`. Subsequent runs are instant.
|
||||
- **Tool-calling reliability** with Qwen3-Coder is decent but not
|
||||
Claude-grade. If a tool call hangs or returns malformed JSON, the
|
||||
model is the culprit, not the MCP. Worth trying the same prompt
|
||||
against a hosted Claude or GPT-5 to confirm before debugging the
|
||||
server.
|
||||
- **Adding more MCP servers**: drop another entry under the `mcp` key
|
||||
using the same `type/command/enabled` shape. The
|
||||
[official MCP registry](https://registry.modelcontextprotocol.io/)
|
||||
and [Awesome MCP Servers](https://mcpservers.org/) catalog options.
|
||||
145
opencode/install.sh
Executable file
145
opencode/install.sh
Executable file
@@ -0,0 +1,145 @@
|
||||
#!/usr/bin/env bash
|
||||
# Bootstrap or re-sync the OpenCode harness on this Mac.
|
||||
#
|
||||
# Idempotent — re-run after pulling changes to opencode.json or the
|
||||
# Phoenix bridge plugin. Each step checks before doing work.
|
||||
#
|
||||
# What this does:
|
||||
# 1. Verify Homebrew is present
|
||||
# 2. Install node, uv, opencode, jq (skips if already at latest)
|
||||
# 3. Pre-cache Playwright's chromium so the first MCP call is instant
|
||||
# 4. Install the Phoenix bridge plugin's OTel deps
|
||||
# 5. Generate ~/.config/opencode/opencode.json from the repo's
|
||||
# opencode.json with relative plugin paths rewritten to absolute,
|
||||
# so opencode loads the plugin regardless of where it's launched.
|
||||
#
|
||||
# Usage: ./install.sh (from this directory)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
HERE="$(pwd)"
|
||||
|
||||
# --- Pretty printing ---------------------------------------------------------
|
||||
bold() { printf '\033[1m%s\033[0m\n' "$*"; }
|
||||
ok() { printf ' \033[32m✓\033[0m %s\n' "$*"; }
|
||||
info() { printf ' → %s\n' "$*"; }
|
||||
warn() { printf ' \033[33m!\033[0m %s\n' "$*"; }
|
||||
fail() { printf ' \033[31m✗\033[0m %s\n' "$*"; exit 1; }
|
||||
|
||||
# --- 1. Homebrew -------------------------------------------------------------
|
||||
bold "[1/5] Homebrew"
|
||||
if ! command -v brew >/dev/null 2>&1; then
|
||||
fail "brew not found. Install from https://brew.sh, then re-run."
|
||||
fi
|
||||
ok "brew $(brew --version | head -1 | awk '{print $2}')"
|
||||
|
||||
# --- 2. CLI deps -------------------------------------------------------------
|
||||
bold "[2/5] CLI dependencies"
|
||||
brew_install_if_missing() {
|
||||
local pkg="$1"
|
||||
local bin="${2:-$1}"
|
||||
if command -v "$bin" >/dev/null 2>&1; then
|
||||
ok "$pkg already installed ($(command -v "$bin"))"
|
||||
else
|
||||
info "installing $pkg"
|
||||
brew install "$pkg"
|
||||
ok "$pkg installed"
|
||||
fi
|
||||
}
|
||||
brew_install_if_missing node node
|
||||
brew_install_if_missing uv uv
|
||||
brew_install_if_missing jq jq
|
||||
# opencode is in a tap; check the binary, not the formula name.
|
||||
if command -v opencode >/dev/null 2>&1; then
|
||||
ok "opencode already installed ($(command -v opencode))"
|
||||
else
|
||||
info "tapping sst/tap and installing opencode"
|
||||
brew install sst/tap/opencode
|
||||
ok "opencode installed"
|
||||
fi
|
||||
|
||||
# --- 3. Playwright browsers --------------------------------------------------
|
||||
bold "[3/5] Playwright browser cache"
|
||||
PW_CACHE="${HOME}/Library/Caches/ms-playwright"
|
||||
if [[ -d "$PW_CACHE" ]] && find "$PW_CACHE" -name "chrome" -o -name "Chromium*" 2>/dev/null | grep -q .; then
|
||||
ok "browsers already cached at $PW_CACHE"
|
||||
else
|
||||
info "downloading chromium (~200 MB) — first run only"
|
||||
npx -y @playwright/mcp@latest --help >/dev/null 2>&1 || true
|
||||
ok "browsers cached"
|
||||
fi
|
||||
|
||||
# --- 4. Phoenix bridge plugin deps ------------------------------------------
|
||||
bold "[4/5] Phoenix bridge plugin deps"
|
||||
if [[ -d ".opencode/plugin/node_modules" && -f ".opencode/plugin/package-lock.json" ]]; then
|
||||
# Re-run npm install if package.json is newer than the lockfile, otherwise skip.
|
||||
if [[ ".opencode/plugin/package.json" -nt ".opencode/plugin/package-lock.json" ]]; then
|
||||
info "package.json is newer than lockfile — running npm install"
|
||||
( cd .opencode/plugin && npm install )
|
||||
ok "deps updated"
|
||||
else
|
||||
ok "deps already installed"
|
||||
fi
|
||||
else
|
||||
info "installing OTel deps (one-time, ~40 MB)"
|
||||
( cd .opencode/plugin && npm install )
|
||||
ok "deps installed"
|
||||
fi
|
||||
|
||||
# --- 5. Generate ~/.config/opencode/opencode.json ---------------------------
|
||||
# The repo's opencode.json uses relative plugin paths so it stays valid
|
||||
# in-place. Rewriting them to absolute paths here makes opencode find the
|
||||
# plugin regardless of which directory it was launched from. Re-run this
|
||||
# script after editing opencode.json.
|
||||
bold "[5/5] Deploy global config"
|
||||
mkdir -p "${HOME}/.config/opencode"
|
||||
src="${HERE}/opencode.json"
|
||||
dst="${HOME}/.config/opencode/opencode.json"
|
||||
|
||||
# If the user previously had a symlink from the old install.sh, replace it.
|
||||
if [[ -L "$dst" ]]; then
|
||||
info "removing stale symlink at $dst"
|
||||
rm "$dst"
|
||||
fi
|
||||
# And the old .opencode dir symlink — no longer needed now that plugin
|
||||
# paths are absolute.
|
||||
if [[ -L "${HOME}/.config/opencode/.opencode" ]]; then
|
||||
info "removing stale ~/.config/opencode/.opencode symlink"
|
||||
rm "${HOME}/.config/opencode/.opencode"
|
||||
fi
|
||||
|
||||
# Rewrite any relative plugin path (./foo, ../foo) to an absolute path
|
||||
# rooted at this directory. Absolute paths and npm-package refs pass
|
||||
# through untouched.
|
||||
jq --arg here "$HERE" '
|
||||
.plugin = (
|
||||
(.plugin // [])
|
||||
| map(
|
||||
if type == "string" and (startswith("./") or startswith("../"))
|
||||
then ($here + "/" + ltrimstr("./") | gsub("/\\./"; "/"))
|
||||
else .
|
||||
end
|
||||
)
|
||||
)
|
||||
' "$src" > "$dst.tmp"
|
||||
mv "$dst.tmp" "$dst"
|
||||
ok "wrote $dst"
|
||||
info "plugin paths resolved to:"
|
||||
jq -r '.plugin[]?' "$dst" | sed 's/^/ /'
|
||||
|
||||
echo
|
||||
bold "Done."
|
||||
cat <<EOF
|
||||
|
||||
Re-run this script after editing opencode.json — the deployed copy at
|
||||
~/.config/opencode/opencode.json is generated, not symlinked.
|
||||
|
||||
Next steps:
|
||||
- Verify the model server: curl -s http://framework:11434/v1/models | jq '.data[].id'
|
||||
- Verify Phoenix is up: curl -sf http://framework:6006/
|
||||
- Run opencode and send one prompt. A trace should appear at
|
||||
http://framework:6006 within a couple of seconds.
|
||||
- If nothing appears, check ~/.local/share/opencode/log/*.log for a
|
||||
line starting with "[phoenix-bridge]".
|
||||
EOF
|
||||
41
opencode/opencode.json
Normal file
41
opencode/opencode.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
"experimental": {
|
||||
"openTelemetry": true
|
||||
},
|
||||
"plugin": ["./.opencode/plugin/phoenix-bridge.js"],
|
||||
"provider": {
|
||||
"framework": {
|
||||
"npm": "@ai-sdk/openai-compatible",
|
||||
"name": "Framework Desktop (Strix Halo)",
|
||||
"options": {
|
||||
"baseURL": "http://framework:11434/v1"
|
||||
},
|
||||
"models": {
|
||||
"qwen3-coder:30b": {
|
||||
"name": "Qwen3 Coder 30B (local)",
|
||||
"limit": {
|
||||
"context": 131072,
|
||||
"output": 16384
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"mcp": {
|
||||
"playwright": {
|
||||
"type": "local",
|
||||
"command": ["npx", "-y", "@playwright/mcp@latest"],
|
||||
"enabled": true
|
||||
},
|
||||
"searxng": {
|
||||
"type": "local",
|
||||
"command": ["npx", "-y", "mcp-searxng"],
|
||||
"enabled": true,
|
||||
"environment": {
|
||||
"SEARXNG_URL": "https://searxng.n0n.io"
|
||||
}
|
||||
}
|
||||
},
|
||||
"model": "framework/qwen3-coder:30b"
|
||||
}
|
||||
Reference in New Issue
Block a user