Repository navigation
feat(proxy): OpenAI-compatible endpoint + OpenCode auto-config - #1029
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Review limit reached
Your plan includes 2 reviews of capacity. Refill in 25 minutes and 11 seconds. Your organization has run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After more review capacity refills, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than trial, open-source, and free plans. In all cases, review capacity refills continuously over time. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (13)
📝 WalkthroughWalkthroughThis PR adds OpenAI-compatible HTTP proxy support to the NeuroLink proxy system, enabling OpenCode clients to use the proxy alongside existing Claude Code integration. The implementation includes full request/response translation between OpenAI and Claude formats, a shared translation engine serving both proxy variants, and OpenCode auto-configuration during proxy startup. ChangesOpenCode and OpenAI proxy support
🎯 4 (Complex) | ⏱️ ~60 minutes 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
✅ Single Commit Policy - COMPLIANTStatus: Policy requirements met • 1 commit • Valid format • Ready for merge 📊 View validation details📝 Commit Details
✅ Validation Results
🤖 Automated validation by NeuroLink Single Commit Enforcement |
🤖 AI Review & Build Compliance ✅Status: AI analysis complete • Build rules validated • Ready for review 📊 View detailed analysis results🛡️ Analysis Complete
📋 Ready for Merge When
🤖 AI analysis complete - check individual code comments for specific feedback |
Documentation Validation Results🚀 Documentation validation passed!
📦 Build artifact uploaded successfully. Ready for deployment preview. Commit: |
There was a problem hiding this comment.
Pull request overview
Adds an OpenAI-compatible surface to the existing proxy so OpenCode (and other OpenAI clients) can talk to NeuroLink via /v1/chat/completions, while refactoring the Claude translation logic into a shared translation engine and extending proxy setup/start to auto-configure OpenCode.
Changes:
- Introduces OpenAI-compatible endpoints:
POST /v1/chat/completionsandGET /v1/models, including OpenAI↔Claude translation and SSE streaming. - Refactors the “translation mode” implementation into a shared
proxyTranslationEngineused by both Claude and OpenAI proxy routes. - Adds OpenCode auto-configuration in the CLI and ships OpenCode fixture configs + a design/manual test doc.
Reviewed changes
Copilot reviewed 12 out of 12 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| test/fixtures/opencode-local-proxy.json | Adds an OpenCode config fixture targeting the local proxy (Claude passthrough). |
| test/fixtures/opencode-local-proxy-openai-route.json | Adds an OpenCode fixture that forces the OpenAI-compatible /v1/chat/completions path via a non-claude alias. |
| src/lib/types/server.ts | Adds unified proxy flag + openaiProxy option to route creation options. |
| src/lib/types/proxy.ts | Adds shared proxy-format types plus OpenAI Chat Completions wire types. |
| src/lib/server/routes/openaiProxyRoutes.ts | New OpenAI-compatible proxy routes and Anthropic loopback bridge. |
| src/lib/server/routes/index.ts | Registers OpenAI proxy routes and introduces unified proxy enabling logic. |
| src/lib/server/routes/claudeProxyRoutes.ts | Switches Claude translation mode to use the shared translation engine and unifies /v1/models response builder. |
| src/lib/proxy/proxyTranslationEngine.ts | New shared translation engine for stream/json flows + shared /v1/models builder. |
| src/lib/proxy/openaiFormat.ts | New OpenAI format parser/serializer + SSE serializer + Claude→OpenAI SSE transform + error envelope. |
| src/lib/proxy/modelRouter.ts | Exposes model mappings/passthrough lists for /v1/models. |
| src/cli/commands/proxy.ts | Adds OpenCode auto-configuration; registers OpenAI proxy routes in the CLI proxy runtime. |
| docs/features/opencode-proxy-support.md | Design + implementation notes + manual verification playbook. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| return buildOpenAIError( | ||
| 400, | ||
| "Request must include 'model' and 'messages' fields", |
| // Derive loopback URL from the incoming host header so we hit the same | ||
| // proxy process that is handling this request. | ||
| const hostHeader = | ||
| ctx.headers["host"] ?? ctx.headers["Host"] ?? "127.0.0.1:5555"; | ||
| const [rawHost, rawPort] = hostHeader.split(":"); | ||
| const loopbackHost = | ||
| rawHost === "localhost" || !rawHost ? "127.0.0.1" : rawHost; | ||
| const loopbackPort = rawPort ?? "5555"; | ||
| const internalUrl = `http://${loopbackHost}:${loopbackPort}/v1/messages`; | ||
|
|
| provider?: string, | ||
| model?: string, | ||
| ): boolean { | ||
| return provider === "vertex" && model === "gemini-2.5-flash"; |
| const defaultModels = [ | ||
| "claude-sonnet-4-20250514", | ||
| "claude-haiku-3.5-20241022", | ||
| "gpt-4o", | ||
| "gemini-2.5-pro", | ||
| "gemini-2.5-flash", | ||
| ]; |
| "[proxy] Failed to auto-configure OpenCode: " + | ||
| (error instanceof Error ? error.message : String(error)), | ||
| ); | ||
| } |
| /** Snapshot of the original provider.neurolink value before proxy overwrite */ | ||
| let _openCodeOriginalProvider: unknown = undefined; | ||
|
|
||
| async function setOpenCodeProxySettings( | ||
| baseUrl: string, | ||
| proxyKey?: string, | ||
| ): Promise<void> { | ||
| const fs = await import("fs"); | ||
|
|
||
| const configDir = getOpenCodeConfigDir(); | ||
| try { | ||
| fs.accessSync(configDir); | ||
| } catch { | ||
| // OpenCode not installed — config directory does not exist, skip silently | ||
| return; | ||
| } | ||
|
|
||
| let config: Record<string, unknown>; | ||
| try { | ||
| config = JSON.parse(fs.readFileSync(OPENCODE_CONFIG_PATH, "utf8")); | ||
| } catch { | ||
| // file missing/invalid — create fresh config object | ||
| config = { provider: {} }; | ||
| } | ||
|
|
||
| const provider = (config.provider ?? {}) as Record<string, unknown>; | ||
|
|
||
| // Save original provider.neurolink value for restoration (only once) | ||
| if (_openCodeOriginalProvider === undefined) { | ||
| _openCodeOriginalProvider = | ||
| "neurolink" in provider | ||
| ? JSON.parse(JSON.stringify(provider.neurolink)) | ||
| : null; | ||
| } |
| export function createOpenAIProxyRoutes( | ||
| modelRouter?: ModelRouter, | ||
| basePath: string = "", | ||
| ): RouteGroup { | ||
| return { | ||
| prefix: `${basePath}/v1`, | ||
| routes: [ | ||
| // ================================================================= | ||
| // POST /v1/chat/completions — Main chat completions endpoint | ||
| // ================================================================= | ||
| { | ||
| method: "POST", | ||
| path: `${basePath}/v1/chat/completions`, | ||
| description: "OpenAI-compatible chat completions (translation mode)", | ||
| handler: async (ctx: ServerContext) => { | ||
| const requestStartTime = Date.now(); | ||
| const body = ctx.body as OpenAICompletionRequest | undefined; |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
There was a problem hiding this comment.
Actionable comments posted: 10
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/lib/server/routes/claudeProxyRoutes.ts (1)
4604-4618:⚠️ Potential issue | 🟠 Major | ⚡ Quick winKeep Claude
/v1/modelsAnthropic-compatible.This route still sits on the Claude-compatible surface, but it now advertises and returns OpenAI list format. Claude clients expecting the Anthropic models schema will break against
/v1/models; keep the OpenAI list builder on the OpenAI route and use a Claude-specific serializer here.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/lib/server/routes/claudeProxyRoutes.ts` around lines 4604 - 4618, The Claude route handler for GET `${basePath}/v1/models` is incorrectly returning the OpenAI-style model list via buildModelsListResponse(modelRouter); replace that call with the Claude/Anthropic-specific serializer (e.g., buildAnthropicModelsListResponse or a new function that emits the Anthropic models schema) so this endpoint returns Anthropic-compatible model objects, leaving buildModelsListResponse(modelRouter) used only on the OpenAI route; update the handler in claudeProxyRoutes.ts to call the Anthropic serializer and wire it to the same modelRouter for consistency.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/cli/commands/proxy.ts`:
- Around line 343-345: The snapshot in _openCodeOriginalProvider must be
persisted to durable storage so cleanup/restores that run in other processes
(cleanup paths and the fail-open guard) can restore provider.neurolink; modify
the code that currently assigns _openCodeOriginalProvider to also serialize and
write the value atomically to a temp file (e.g., JSON in OS tmp) and update all
restore/cleanup code paths to first attempt to read and parse that temp file and
restore provider.neurolink from it (fall back to the in-memory
_openCodeOriginalProvider if file missing), then remove the temp file after a
successful restore; ensure reads/writes handle errors and use a stable filename
keyed to the runtime/instance so concurrent runs do not conflict.
- Around line 1602-1611: Wrap the auto-configuration call so it does not run in
dev mode: check the dev flag used by this command (e.g., options.dev or
isDev/isDevMode) and if true skip calling setOpenCodeProxySettings(`${url}/v1`)
and instead log the existing "skipping client auto-configuration" message;
otherwise keep the try/catch and logger.always calls as-is. Ensure you
remove/avoid any code path that would write opencode.json in dev mode by
preventing setOpenCodeProxySettings from being invoked (reference
setOpenCodeProxySettings and the surrounding try/catch and logger.always
statements).
In `@src/lib/proxy/openaiFormat.ts`:
- Around line 951-957: The flush() implementation finalizes the SSE transform
without draining any remaining bytes from the TextDecoder, which can drop
trailing buffered bytes; update flush(controller) in openaiFormat.ts to, before
marking finished and emitting serializer.finish(stopReason, usage), call
decoder.decode(undefined, { stream: false }) (or decoder.decode() to drain) to
collect any pending decoded text, process/emit that buffered output via the same
emit/serializer path, then proceed to set finished = true and emit the final
serializer.finish call so no decoder bytes are lost; reference the flush
function, decoder, finished flag, emit(...) and serializer.finish(stopReason,
usage) in your changes.
- Line 931: Move the assignment out of the while condition to avoid inline
assignment: compute sepIdx using buffer.indexOf("\n\n") before the loop (e.g.,
let sepIdx = buffer.indexOf("\n\n");) and then use while (sepIdx !== -1) { ... }
updating sepIdx inside the loop after processing; update the code around the
loop in openaiFormat.ts where sepIdx is used to ensure sepIdx is recalculated at
the end of each iteration rather than assigned in the loop condition.
In `@src/lib/proxy/proxyTranslationEngine.ts`:
- Line 682: Replace the raw throw new Error(lastAttemptError) with a typed error
created via ErrorFactory to match project error patterns: locate the throw
expression (throw new Error(lastAttemptError)) in proxyTranslationEngine.ts and
change it to use ErrorFactory (e.g., ErrorFactory.create or the project-standard
factory method) to build a specific translation failure error, passing
lastAttemptError (and any original error/context) into the factory so the
resulting error is typed and preserves the original message/details.
- Around line 363-365: The call to ctx.neurolink.stream(...) needs to be wrapped
with the withTimeout utility so upstream stalls can't hang request handling:
replace the direct awaits that set streamResult (the occurrence assigning
streamResult in the streaming path) and the similar call around the JSON path
(the call at the second location around lines 577-579) with await withTimeout(()
=> ctx.neurolink.stream(...), timeoutMs) or the project's withTimeout signature,
using the appropriate timeout value (e.g., from options or request config);
ensure you import/use withTimeout and preserve the original arguments passed to
ctx.neurolink.stream so behavior is identical except for the timeout guard.
- Line 488: The tracer span is always ended with a hardcoded 200 which
misrepresents actual outcomes; update the tracer ending call in
proxyTranslationEngine (the tracer?.end(...) invocation) to use the real HTTP
status produced (use the variable responseStatus or the final status you compute
for the request) instead of 200 — e.g. call tracer?.end(responseStatus ?? 500,
Date.now() - requestStartTime) or otherwise pass the computed responseStatus so
traces match logged responseStatus.
In `@src/lib/server/routes/openaiProxyRoutes.ts`:
- Around line 177-185: The upstream error text (errText) and any message are
logged and stored raw; sanitize them using the shared
transformParamsForLogging() before calling logger.always(), writeLifecycle(), or
logRequest(). In the block handling upstream responses (refer to upstream,
errText, logger.always, writeLifecycle, and logRequest), pass the error payload
through transformParamsForLogging(errText) (or wrap into an object like { error:
errText } and transform) and use the sanitized result when building the log
message and lifecycle payload; do the same for the other occurrence around lines
300-314 to ensure no raw upstream content or secrets are persisted.
- Around line 145-153: The code currently builds loopback URL from the
client-controlled Host header (hostHeader, rawHost, loopbackHost, loopbackPort,
internalUrl) and hardcodes "/v1/messages"; replace this by accepting a trusted
loopback origin (e.g., loopbackOrigin) and the configured basePath as parameters
to the helper used in openaiProxyRoutes so the URL is constructed from those
trusted values (use loopbackOrigin + basePath and append the route dynamically
to support both OpenAI and Claude paths) instead of parsing ctx.headers; also
update the other occurrence referenced (the block around lines 171-175) to use
the same trusted parameters.
- Around line 171-175: The POST to the internalUrl that assigns to upstream
(const upstream = await fetch(internalUrl, { ... })) must be wrapped with the
shared withTimeout utility to prevent a hung self-call from stalling the outer
/v1/chat/completions request; replace the direct await fetch(internalUrl, ...)
used in the bridge to /v1/messages with await withTimeout(fetch(internalUrl, {
method: "POST", headers: forwardHeaders, body: JSON.stringify({ ...claudeBody,
stream }) }), timeoutMs) using the existing withTimeout function and appropriate
timeout value so the internal upstream fetch fails fast and returns a controlled
error.
---
Outside diff comments:
In `@src/lib/server/routes/claudeProxyRoutes.ts`:
- Around line 4604-4618: The Claude route handler for GET
`${basePath}/v1/models` is incorrectly returning the OpenAI-style model list via
buildModelsListResponse(modelRouter); replace that call with the
Claude/Anthropic-specific serializer (e.g., buildAnthropicModelsListResponse or
a new function that emits the Anthropic models schema) so this endpoint returns
Anthropic-compatible model objects, leaving buildModelsListResponse(modelRouter)
used only on the OpenAI route; update the handler in claudeProxyRoutes.ts to
call the Anthropic serializer and wire it to the same modelRouter for
consistency.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 5873a838-f204-4c73-9c38-0b123df88084
📒 Files selected for processing (12)
docs/features/opencode-proxy-support.mdsrc/cli/commands/proxy.tssrc/lib/proxy/modelRouter.tssrc/lib/proxy/openaiFormat.tssrc/lib/proxy/proxyTranslationEngine.tssrc/lib/server/routes/claudeProxyRoutes.tssrc/lib/server/routes/index.tssrc/lib/server/routes/openaiProxyRoutes.tssrc/lib/types/proxy.tssrc/lib/types/server.tstest/fixtures/opencode-local-proxy-openai-route.jsontest/fixtures/opencode-local-proxy.json
| try { | ||
| await setOpenCodeProxySettings(`${url}/v1`); | ||
| logger.always(chalk.green(" ✓ Auto-configured OpenCode settings")); | ||
| logger.always(chalk.dim(" Restart OpenCode to connect through proxy")); | ||
| } catch (error) { | ||
| logger.debug( | ||
| "[proxy] Failed to auto-configure OpenCode: " + | ||
| (error instanceof Error ? error.message : String(error)), | ||
| ); | ||
| } |
There was a problem hiding this comment.
Skip OpenCode auto-configuration in --dev mode.
This still writes opencode.json even though --dev is documented and logged as “skipping client auto-configuration”. Because the shutdown cleanup also skips dev mode, that stale OpenCode config will stick around after the dev proxy exits.
🚫 Keep dev mode read-only for client config
- try {
- await setOpenCodeProxySettings(`${url}/v1`);
- logger.always(chalk.green(" ✓ Auto-configured OpenCode settings"));
- logger.always(chalk.dim(" Restart OpenCode to connect through proxy"));
- } catch (error) {
- logger.debug(
- "[proxy] Failed to auto-configure OpenCode: " +
- (error instanceof Error ? error.message : String(error)),
- );
+ if (!isDev) {
+ try {
+ await setOpenCodeProxySettings(`${url}/v1`);
+ logger.always(chalk.green(" ✓ Auto-configured OpenCode settings"));
+ logger.always(chalk.dim(" Restart OpenCode to connect through proxy"));
+ } catch (error) {
+ logger.debug(
+ "[proxy] Failed to auto-configure OpenCode: " +
+ (error instanceof Error ? error.message : String(error)),
+ );
+ }
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| try { | |
| await setOpenCodeProxySettings(`${url}/v1`); | |
| logger.always(chalk.green(" ✓ Auto-configured OpenCode settings")); | |
| logger.always(chalk.dim(" Restart OpenCode to connect through proxy")); | |
| } catch (error) { | |
| logger.debug( | |
| "[proxy] Failed to auto-configure OpenCode: " + | |
| (error instanceof Error ? error.message : String(error)), | |
| ); | |
| } | |
| if (!isDev) { | |
| try { | |
| await setOpenCodeProxySettings(`${url}/v1`); | |
| logger.always(chalk.green(" ✓ Auto-configured OpenCode settings")); | |
| logger.always(chalk.dim(" Restart OpenCode to connect through proxy")); | |
| } catch (error) { | |
| logger.debug( | |
| "[proxy] Failed to auto-configure OpenCode: " + | |
| (error instanceof Error ? error.message : String(error)), | |
| ); | |
| } | |
| } |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/cli/commands/proxy.ts` around lines 1602 - 1611, Wrap the
auto-configuration call so it does not run in dev mode: check the dev flag used
by this command (e.g., options.dev or isDev/isDevMode) and if true skip calling
setOpenCodeProxySettings(`${url}/v1`) and instead log the existing "skipping
client auto-configuration" message; otherwise keep the try/catch and
logger.always calls as-is. Ensure you remove/avoid any code path that would
write opencode.json in dev mode by preventing setOpenCodeProxySettings from
being invoked (reference setOpenCodeProxySettings and the surrounding try/catch
and logger.always statements).
8661742 to
3c0c2eb
Compare
Review Feedback Addressed (Cycle 1)Pushed What changed, per finding
Files modified
Validation
Re-review requested@coderabbitai review cc Copilot — please re-run on the latest commit. All 18 distinct findings from the previous pass have been addressed; some (CR-B specifically) were adapted because the suggested helper didn't match the intent. |
|
Triggering a full re-review of commit (^・ω・^)ノ ✅ Actions performedReview triggered.
|
🤖 AI Review & Build Compliance ✅Status: AI analysis complete • Build rules validated • Ready for review 📊 View detailed analysis results🛡️ Analysis Complete
📋 Ready for Merge When
🤖 AI analysis complete - check individual code comments for specific feedback |
|
@coderabbitai review |
✅ Actions performedReview triggered.
|
…uration Adds a /v1/chat/completions endpoint alongside the existing Claude passthrough so OpenCode (and any OpenAI-compatible client) can point at the proxy without format adapters. - Shared translation engine extracted from claudeProxyRoutes into proxyTranslationEngine.ts; both Claude and OpenAI routes now flow through the same attempt-loop, telemetry, and serializer adapter path. - OpenAI <-> internal <-> Claude translation round-trip, including tool-call id preservation, stream/SSE transform, and Claude model bridging via an internal /v1/messages loopback so OAuth passthrough still applies. - OpenCode auto-configuration during `proxy setup` writes the XDG config pointing at the running proxy; `--skip-configure` bypasses launchd/config writes for isolated dev instances. - ProxyFormat and StreamSerializerAdapter types live in src/lib/types/proxy.ts per type-engineering ESLint rules; CreateRoutesOptions gains an `openaiProxy` flag and a unified `proxy` flag that enables both formats.
3c0c2eb to
f3bbc41
Compare
Cycle 2 — CodeQL stack-trace exposure fixPushed Issue: CodeQL flagged information-exposure-through-stack-trace at Fix: The two catch blocks (loopback exception @ ~331, outer translation exception @ ~418) now return fixed generic messages to the client ( Upstream-status passthrough (line 216) and the validation 400 (line 299) were not touched — their messages are upstream-controlled or static, not from a caught exception. Validation
@coderabbitai review |
|
A few quick confirmations on the approach:
Triggering re-review of (^・ω・^)ノ ✅ Actions performedReview triggered.
|
🤖 AI Review & Build Compliance ✅Status: AI analysis complete • Build rules validated • Ready for review 📊 View detailed analysis results🛡️ Analysis Complete
📋 Ready for Merge When
🤖 AI analysis complete - check individual code comments for specific feedback |
All Reviewers Clean — Ready for MergeTwo fix cycles completed. Current commit: Verification snapshot
Cycles summaryCycle 1 — Cycle 2 — Files touched (final)
PR ready for merge. |
|
🎉 This PR is included in version 9.66.0 🎉 The release is available on: Your semantic-release bot 📦🚀 |
Summary
POST /v1/chat/completions+GET /v1/modelsendpoints to the proxy with full request / response / SSE / tool-call / tool-result format translation (src/lib/proxy/openaiFormat.ts,src/lib/server/routes/openaiProxyRoutes.ts)./v1/messagesvia internalfetch()to reuse OAuth account rotation, retry, and SSE intercept — parent lifecycle is logged asaccountType: openai-bridgeand correlates 1:1 with the child/v1/messageslog entry (matchingrequestIdnot, but matching timestamp + token counts).proxy setupnow auto-configures the OpenCode CLI alongside Claude Code; new--devflag isolates state to.neurolink-dev/so testing the proxy doesn't touch the user's launchd-managed instance. Two fixtures land for manual + automated testing:test/fixtures/opencode-local-proxy.json(Claude passthrough) andtest/fixtures/opencode-local-proxy-openai-route.json(gpt-4o alias mapped back to claude-haiku via router config — this is the one that exercises the new/v1/chat/completionsbranch, since OpenCode 1.3.x routes any model name containingclaudestraight through its bundled Anthropic SDK to/v1/messages).Implementation reference:
docs/features/opencode-proxy-support.md(full design + §11 manual test plan).Test plan
GET /health,GET /v1/models, non-stream + streamPOST /v1/chat/completionsopencode-local-proxy-openai-route.jsonfixtureaccountType: openai-bridge, path/v1/chat/completions) + child (/v1/messages) entries with matching input/output token countspnpm run check,pnpm run lint,pnpm run build:cli,pnpm test--devmode does not clobber~/.config/opencode/opencode.jsonor the launchd proxy on :55669Summary by CodeRabbit
New Features
/v1/chat/completionsand/v1/models).Documentation