docs: implementation plan for provider-list discovery

This commit is contained in:
Troed Sångberg
2026-08-21 12:18:04 +02:00
parent f3cd09a65d
commit 0ecafb8d8a
@@ -0,0 +1,297 @@
# Provider-List Discovery Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Base llama-server detection on OpenCode's resolved provider list (`api.state.provider`) instead of raw config parsing, keeping the legacy config parse as fallback, and bump the version to 1.3.0.
**Architecture:** New pure function `extractProviderUrlsFromList(providers)` matches SDK-shaped `{ id, name, options }` records against "llama"/"ollama". `resolveServerUrls(options, providers, config)` gains a parameter: explicit `server` → provider list → legacy config parse → localhost fallback. `tui.tsx` passes `api.state.provider`.
**Tech Stack:** TypeScript, `node:test` + `tsx` (existing pattern in `test-backoff.ts`).
**Spec:** `docs/superpowers/specs/2026-08-21-provider-list-discovery-design.md`
---
### Task 1: `extractProviderUrlsFromList` and extended `resolveServerUrls`
**Files:**
- Modify: `src/stats.ts`
- Test: `test-backoff.ts`
- [ ] **Step 1: Rewrite the tests**
In `test-backoff.ts`, replace every existing `resolveServerUrls` test and the `llamaConfig` fixture with the following (keep the `backoffDelayMs` import and its tests untouched):
```ts
import { backoffDelayMs, resolveServerUrls } from "./src/stats.ts"
const llamaProviders = [
{
id: "llamacpp",
name: "llama.cpp",
source: "config",
env: [],
options: { baseURL: "http://localhost:9090/v1" },
models: {},
},
]
const visionProviders = [
{
id: "vision",
name: "vision",
source: "config",
env: [],
options: { baseURL: "http://headless.local:8080/v1" },
models: {},
},
]
const llamaConfig = {
provider: {
llama: { options: { baseURL: "http://localhost:9090/v1" } },
},
}
test("explicit server wins over provider list", () => {
assert.deepEqual(
resolveServerUrls({ server: "http://headless.local:8080" }, llamaProviders, {}),
["http://headless.local:8080"],
)
})
test("explicit server strips /v1 path", () => {
assert.deepEqual(
resolveServerUrls({ server: "http://headless.local:8080/v1" }, llamaProviders, {}),
["http://headless.local:8080"],
)
})
test("non-string server falls back to provider list", () => {
assert.deepEqual(resolveServerUrls({ server: 42 }, llamaProviders, {}), [
"http://localhost:9090",
])
})
test("empty server falls back to provider list", () => {
assert.deepEqual(resolveServerUrls({ server: "" }, llamaProviders, {}), [
"http://localhost:9090",
])
})
test("scheme-less server falls back to provider list", () => {
assert.deepEqual(resolveServerUrls({ server: "localhost:8080" }, llamaProviders, {}), [
"http://localhost:9090",
])
})
test("whitespace-only server falls back to provider list", () => {
assert.deepEqual(resolveServerUrls({ server: " " }, llamaProviders, {}), [
"http://localhost:9090",
])
})
test("null server falls back to provider list", () => {
assert.deepEqual(resolveServerUrls({ server: null }, llamaProviders, {}), [
"http://localhost:9090",
])
})
test("no options uses provider list", () => {
assert.deepEqual(resolveServerUrls(undefined, llamaProviders, {}), [
"http://localhost:9090",
])
})
test("provider name match detects llama servers", () => {
const byName = [{ ...visionProviders[0], name: "My llama server" }]
assert.deepEqual(resolveServerUrls(undefined, byName, {}), [
"http://headless.local:8080",
])
})
test("ollama providers are excluded", () => {
const ollama = [
{ id: "ollama", name: "Ollama", source: "config", env: [], options: { baseURL: "http://localhost:11434" }, models: {} },
]
assert.deepEqual(resolveServerUrls(undefined, ollama, {}), ["http://localhost:8080"])
})
test("non-string baseURL is ignored", () => {
const bad = [{ id: "llamacpp", name: "llama.cpp", options: { baseURL: 123 } }]
assert.deepEqual(resolveServerUrls(undefined, bad, {}), ["http://localhost:8080"])
})
test("missing provider list falls back to legacy config parse", () => {
assert.deepEqual(resolveServerUrls(undefined, undefined, llamaConfig), [
"http://localhost:9090",
])
})
test("empty provider list falls back to legacy config parse", () => {
assert.deepEqual(resolveServerUrls(undefined, [], llamaConfig), [
"http://localhost:9090",
])
})
test("nothing anywhere falls back to localhost", () => {
assert.deepEqual(resolveServerUrls(undefined, visionProviders, {}), [
"http://localhost:8080",
])
})
```
- [ ] **Step 2: Run tests to verify they fail**
Run: `npx -y tsx test-backoff.ts`
Expected: FAIL — `resolveServerUrls` called with wrong argument counts / missing export `extractProviderUrlsFromList` behavior
- [ ] **Step 3: Implement**
In `src/stats.ts`, add after `extractProviderUrls`:
```ts
export function extractProviderUrlsFromList(providers: unknown): string[] {
if (!Array.isArray(providers)) return []
const urls: string[] = []
for (const entry of providers) {
if (!entry || typeof entry !== "object") continue
const provider = entry as Record<string, unknown>
const id = typeof provider.id === "string" ? provider.id.toLowerCase() : ""
const name = typeof provider.name === "string" ? provider.name.toLowerCase() : ""
const hay = `${id} ${name}`
if (!hay.includes("llama") || hay.includes("ollama")) continue
const opts = provider.options
if (!opts || typeof opts !== "object") continue
const baseURL = (opts as Record<string, unknown>).baseURL || (opts as Record<string, unknown>).base_url
if (typeof baseURL === "string") {
urls.push(stripBaseUrlPath(baseURL))
}
}
return urls
}
```
Replace the body of `resolveServerUrls` so the full function reads:
```ts
export function resolveServerUrls(options: unknown, providers: unknown, config: unknown): string[] {
const opts = options && typeof options === "object" ? (options as Record<string, unknown>) : undefined
const server = opts?.server
const explicit = typeof server === "string" && server.trim() !== "" ? stripBaseUrlPath(server) : undefined
if (explicit && /^https?:\/\//.test(explicit)) {
return [explicit]
}
if (server !== undefined) {
console.warn("[oc-ls-stats] ignoring invalid 'server' plugin option, falling back to provider detection")
}
const detected = extractProviderUrlsFromList(providers)
if (detected.length > 0) return detected
const legacy = extractProviderUrls(config)
return legacy.length > 0 ? legacy : ["http://localhost:8080"]
}
```
- [ ] **Step 4: Run tests to verify they pass**
Run: `npx -y tsx test-backoff.ts`
Expected: PASS — 5 backoff tests + 14 resolver tests (warn lines expected for invalid-server cases)
- [ ] **Step 5: Commit**
```bash
git add src/stats.ts test-backoff.ts
git commit -m "feat: detect llama-server from resolved provider list"
```
---
### Task 2: Pass `api.state.provider` from `tui.tsx`
**Files:**
- Modify: `tui.tsx` (startup block, ~line 307)
- [ ] **Step 1: Update the call site**
Change:
```ts
llamaServerUrls = resolveServerUrls(options, api.state.config)
```
to:
```ts
llamaServerUrls = resolveServerUrls(options, api.state.provider, api.state.config)
```
- [ ] **Step 2: Verify**
Run: `npx -y tsx test-backoff.ts`
Expected: PASS (19 tests)
Run: `grep -n "resolveServerUrls" tui.tsx`
Expected: exactly two lines — the import and the call site with three arguments
- [ ] **Step 3: Commit**
```bash
git add tui.tsx
git commit -m "feat: pass resolved provider list to server discovery"
```
---
### Task 3: README wording
**Files:**
- Modify: `README.md` ("### Server Discovery" first paragraph)
- [ ] **Step 1: Update the discovery paragraph**
Replace:
```markdown
By default the plugin discovers the llama-server URL by reading the OpenCode configuration via the TUI API and extracting `baseURL`/`base_url` fields from provider options whose name contains "llama" (but excludes providers whose name contains "ollama"). Falls back to `http://localhost:8080` if no matching provider is found.
```
with:
```markdown
By default the plugin discovers the llama-server URL from OpenCode's resolved provider list (`id` or `name` containing "llama", excluding "ollama"), extracting `baseURL`/`base_url` from the provider options. This covers providers from all sources (config, environment, API). If the provider list is unavailable, it falls back to parsing the OpenCode configuration directly, and finally to `http://localhost:8080`.
```
- [ ] **Step 2: Commit**
```bash
git add README.md
git commit -m "docs: describe provider-list based discovery"
```
---
### Task 4: Version bump and final verification
**Files:**
- Modify: `package.json`
- [ ] **Step 1: Bump version**
In `package.json`, change `"version": "1.2.2"` to `"version": "1.3.0"`.
- [ ] **Step 2: Run the full prepack suite**
Run: `npm run prepack`
Expected: all 19 tests PASS, exit code 0
- [ ] **Step 3: Commit**
```bash
git add package.json
git commit -m "chore: v1.3.0 — explicit server option and provider-list discovery"
```
- [ ] **Step 4: Confirm clean tree**
Run: `git status --short`
Expected: no output