Skip to content

feat(proxy): OpenAI-compatible endpoint + OpenCode auto-config - #1029

Merged
murdore merged 1 commit into
releasefrom
feat/opencode-support-for-proxy
May 24, 2026
Merged

murdore merged 1 commit into
releasefrom
feat/opencode-support-for-proxy

Conversation

@murdore

@murdore murdore commented May 24, 2026 •

Copy link
Copy Markdown
Contributor

Summary

  • Adds OpenAI-compatible POST /v1/chat/completions + GET /v1/models endpoints 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).
  • When the resolved target is Anthropic, the request loops through the proxy's own /v1/messages via internal fetch() to reuse OAuth account rotation, retry, and SSE intercept — parent lifecycle is logged as accountType: openai-bridge and correlates 1:1 with the child /v1/messages log entry (matching requestId not, but matching timestamp + token counts).
  • proxy setup now auto-configures the OpenCode CLI alongside Claude Code; new --dev flag 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) and test/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/completions branch, since OpenCode 1.3.x routes any model name containing claude straight through its bundled Anthropic SDK to /v1/messages).

Implementation reference: docs/features/opencode-proxy-support.md (full design + §11 manual test plan).

Test plan

  • Smoke: GET /health, GET /v1/models, non-stream + stream POST /v1/chat/completions
  • OpenCode E2E flow §11.8 (E1–E10) using opencode-local-proxy-openai-route.json fixture
  • Negative test (§11.10): kill proxy → OpenCode request times out; restart → success — proves OpenCode is actually flowing through the proxy, not direct to Anthropic
  • Verify proxy JSONL logs show paired parent (accountType: openai-bridge, path /v1/chat/completions) + child (/v1/messages) entries with matching input/output token counts
  • CI: pnpm run check, pnpm run lint, pnpm run build:cli, pnpm test
  • Manual confirmation that --dev mode does not clobber ~/.config/opencode/opencode.json or the launchd proxy on :55669

Summary by CodeRabbit

  • New Features

    • OpenCode can now be automatically configured when the proxy starts and stops.
    • OpenAI-compatible endpoints are now available (/v1/chat/completions and /v1/models).
    • Proxy automatically routes OpenAI requests to the appropriate backend.
  • Documentation

    • Added comprehensive guide for OpenCode proxy integration including setup, configuration, and verification steps.

Review Change Stack

Copilot AI review requested due to automatic review settings May 24, 2026 14:35
@vercel

vercel Bot commented May 24, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
neurolink Ready Ready Preview, Comment May 24, 2026 8:24pm

@coderabbitai

coderabbitai Bot commented May 24, 2026 •

Copy link
Copy Markdown

Warning

Review limit reached

@murdore, we couldn't start this review because you've used your available PR reviews for now.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 03223a18-8d90-4cc4-a85f-594274f683f4

📥 Commits

Reviewing files that changed from the base of the PR and between 8661742 and f3bbc41.

📒 Files selected for processing (13)
  • docs/features/opencode-proxy-support.md
  • src/cli/commands/proxy.ts
  • src/lib/proxy/modelRouter.ts
  • src/lib/proxy/openaiFormat.ts
  • src/lib/proxy/proxyTranslationEngine.ts
  • src/lib/server/routes/claudeProxyRoutes.ts
  • src/lib/server/routes/index.ts
  • src/lib/server/routes/openaiProxyRoutes.ts
  • src/lib/types/proxy.ts
  • src/lib/types/server.ts
  • test/continuous-test-suite-proxy-openai-format.ts
  • test/fixtures/opencode-local-proxy-openai-route.json
  • test/fixtures/opencode-local-proxy.json
📝 Walkthrough

Walkthrough

This 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.

Changes

OpenCode and OpenAI proxy support

Layer / File(s) Summary
Type definitions for OpenAI and proxy formats
src/lib/types/proxy.ts, src/lib/types/server.ts
Defines ProxyFormat union, StreamSerializerAdapter interface, and complete OpenAI Chat Completions types including messages, tools, streaming chunks, and ParsedOpenAIRequest intermediate representation.
OpenAI request/response format translation
src/lib/proxy/openaiFormat.ts
Implements OpenAI request parsing, response serialization, error formatting, and streaming: parseOpenAIRequest extracts/flattens messages with image handling; OpenAIStreamSerializer generates streaming frames; conversion functions bridge OpenAI ↔ Claude formats.
Shared format-agnostic translation orchestration
src/lib/proxy/proxyTranslationEngine.ts
Provides unified stream and JSON request handling across Claude and OpenAI formats: buildTranslationOptions constructs provider-specific parameters; handleTranslatedStreamRequest and handleTranslatedJsonRequest manage fallback attempts and serialization; buildModelsListResponse generates /v1/models output.
ModelRouter HTTP accessors
src/lib/proxy/modelRouter.ts
Exposes getModelMappings() and getPassthroughModels() methods for dynamic /v1/models endpoint population.
OpenAI-compatible proxy route endpoints
src/lib/server/routes/openaiProxyRoutes.ts
Implements /v1/chat/completions and /v1/models POST/GET handlers: validates input, resolves target provider/model via ModelRouter, detects Anthropic loopback, parses/adapts OpenAI requests, and delegates to shared translation handlers.
Claude routes refactored for shared translation engine
src/lib/server/routes/claudeProxyRoutes.ts
Updates Claude proxy routes to delegate translation to shared handlers; removes duplicate logic; updates /v1/models to use shared response builder; aliases buildProxyFallbackOptions to shared implementation.
Route registration and CLI integration
src/lib/server/routes/index.ts, src/cli/commands/proxy.ts
Wires OpenAI routes into the server; updates createAllRoutes to enable both proxy variants; adds OpenCode auto-configuration in startup/shutdown/setup with config-file write and env-var fallback; updates startup banner and integration hooks.
Feature documentation and test fixtures
docs/features/opencode-proxy-support.md, test/fixtures/opencode-*.json
Complete feature documentation covering auto-configuration design, OpenAI endpoint architecture, request/response mapping, and comprehensive manual testing playbook; JSON fixtures for local proxy testing with OpenCode.

🎯 4 (Complex) | ⏱️ ~60 minutes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 65.22% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the two main features added: OpenAI-compatible endpoints and OpenCode auto-configuration support in the proxy.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/opencode-support-for-proxy

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions

github-actions Bot commented May 24, 2026 •

Copy link
Copy Markdown
Contributor

✅ Single Commit Policy - COMPLIANT

Status: Policy requirements met • 1 commit • Valid format • Ready for merge

📊 View validation details

📝 Commit Details

  • Hash: f3bbc414afd3eafd7c14de5f71d77b0055745fea
  • Message: feat(proxy): add OpenAI-compatible endpoint with OpenCode auto-configuration
  • Author: Sachin Sharma

✅ Validation Results

  • Single commit requirement met
  • No merge commits in branch
  • Semantic commit message format verified
  • Ready for squash merge to release branch

🤖 Automated validation by NeuroLink Single Commit Enforcement

@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

@github-actions

github-actions Bot commented May 24, 2026 •

Copy link
Copy Markdown
Contributor

Documentation Validation Results

🚀 Documentation validation passed!

Check Status Result
Frontmatter Validation ✅ Passed
TypeScript Check ✅ Passed
Build ✅ Passed
Link Validation ✅ Passed

📦 Build artifact uploaded successfully. Ready for deployment preview.

Commit: 5e673f6d79c25fbe05f35cbe9df6805cb07c76ad | Workflow: View logs

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/completions and GET /v1/models, including OpenAI↔Claude translation and SSE streaming.
  • Refactors the “translation mode” implementation into a shared proxyTranslationEngine used 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.

Comment on lines +264 to +266
return buildOpenAIError(
400,
"Request must include 'model' and 'messages' fields",
Comment on lines +145 to +154
// 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`;

Comment thread src/lib/proxy/proxyTranslationEngine.ts Outdated
provider?: string,
model?: string,
): boolean {
return provider === "vertex" && model === "gemini-2.5-flash";
Comment thread src/lib/proxy/proxyTranslationEngine.ts Outdated
Comment on lines +736 to +742
const defaultModels = [
"claude-sonnet-4-20250514",
"claude-haiku-3.5-20241022",
"gpt-4o",
"gemini-2.5-pro",
"gemini-2.5-flash",
];
Comment thread src/cli/commands/proxy.ts Outdated
Comment on lines +1551 to +1611
"[proxy] Failed to auto-configure OpenCode: " +
(error instanceof Error ? error.message : String(error)),
);
}
Comment thread src/cli/commands/proxy.ts Outdated
Comment on lines +343 to +376
/** 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;
}
Comment on lines +244 to +260
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;
@murdore

murdore commented May 24, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 24, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 win

Keep Claude /v1/models Anthropic-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

📥 Commits

Reviewing files that changed from the base of the PR and between 3684922 and 8661742.

📒 Files selected for processing (12)
  • docs/features/opencode-proxy-support.md
  • src/cli/commands/proxy.ts
  • src/lib/proxy/modelRouter.ts
  • src/lib/proxy/openaiFormat.ts
  • src/lib/proxy/proxyTranslationEngine.ts
  • src/lib/server/routes/claudeProxyRoutes.ts
  • src/lib/server/routes/index.ts
  • src/lib/server/routes/openaiProxyRoutes.ts
  • src/lib/types/proxy.ts
  • src/lib/types/server.ts
  • test/fixtures/opencode-local-proxy-openai-route.json
  • test/fixtures/opencode-local-proxy.json

Comment thread src/cli/commands/proxy.ts Outdated
Comment thread src/cli/commands/proxy.ts Outdated
Comment on lines +1602 to +1611
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)),
);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

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.

Suggested change
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).

Comment thread src/lib/proxy/openaiFormat.ts Outdated
Comment thread src/lib/proxy/openaiFormat.ts
Comment thread src/lib/proxy/proxyTranslationEngine.ts Outdated
Comment thread src/lib/proxy/proxyTranslationEngine.ts Outdated
Comment thread src/lib/proxy/proxyTranslationEngine.ts Outdated
Comment thread src/lib/server/routes/openaiProxyRoutes.ts Outdated
Comment thread src/lib/server/routes/openaiProxyRoutes.ts Outdated
Comment thread src/lib/server/routes/openaiProxyRoutes.ts
@murdore

murdore commented May 24, 2026

Copy link
Copy Markdown
Contributor Author

Review Feedback Addressed (Cycle 1)

Pushed 3c0c2eb4 resolving all 15 distinct issues raised across the previous Copilot (7) and CodeRabbit (11) review passes. Single-commit policy preserved (amended into the existing commit).

What changed, per finding

# Reviewer File:Line Issue Fix
C1 Copilot openaiProxyRoutes.ts (5 sites) buildOpenAIError() returns plain object → CLI runtime defaulted to HTTP 200 for ALL errors Introduced buildOpenAIErrorResponse(status, message) that wraps the envelope in new Response(..., { status }). Every error return now uses this. Regression test added (see "Tests").
C2 / CR-Critical both openaiProxyRoutes.ts:153 SSRF: loopback URL built from client-controlled Host header Hard-coded loopback host to 127.0.0.1. The listener port is plumbed through a new third parameter loopbackPort on createOpenAIProxyRoutes(modelRouter, basePath, loopbackPort), passed from params.port in the CLI handler. Default 55669 for SDK consumers. The Host header is no longer read.
C3 Copilot proxyTranslationEngine.ts:171 shouldOmitThinkingConfigForTarget regressed to a single literal match Restored the broader gating: omit always for litellm; for vertex omit when model is NOT gemini-(2.5|3); pass through otherwise. Matches the pre-refactor behavior.
C4 Copilot proxyTranslationEngine.ts:738 Wrong canonical model ID claude-haiku-3.5-20241022 Replaced inline list with a module-level DEFAULT_MODEL_IDS using the canonical Anthropic form (claude-3-5-haiku-20241022) and added the current-generation 4-5/4-6 variants.
C5 both proxy.ts:1611 OpenCode auto-config ran in --dev mode Moved the OpenCode try/catch block inside the existing if (!isDev) gate (same gate as Claude Code). Dev mode now skips both.
C6 both proxy.ts:344 OpenCode original-provider snapshot held only in process memory → user config destroyed on crash Replaced _openCodeOriginalProvider with __proxy_original_neurolink persisted inside opencode.json, mirroring Claude's __proxy_original_env pattern. Snapshot is taken once on first set(), and clear() now refuses to delete provider.neurolink if no snapshot is present (instead of deleting unconditionally).
C7 Copilot openaiProxyRoutes.ts (file scope) No automated tests for /v1/chat/completions Added test/continuous-test-suite-proxy-openai-format.ts — 10 unit tests covering ID generation, the error envelope (regression for C1), parseOpenAIRequest, convertOpenAIToClaudeRequest, convertClaudeToOpenAIResponse, the SSE stream transform happy path, decoder-flush behavior across chunk boundaries (regression for CR-D), and tool_use → tool_calls translation. All 10 pass.
CR-A (timeout) CodeRabbit openaiProxyRoutes.ts:175 Loopback fetch() had no timeout Wrapped the internal fetch in withTimeout(..., 5 * 60 * 1000) so a stuck inner handler cannot hang the outer request indefinitely.
CR-B (log sanitize) CodeRabbit openaiProxyRoutes.ts:185, 300 errText/message logged raw The suggested helper transformParamsForLogging() is actually an object summarizer, not a secret sanitizer. Used sanitizeForLog() from src/lib/utils/logSanitize.ts instead — it truncates AND redacts secret patterns. Applied at all four log sites in the bridge + catch path.
CR-C (lint) CodeRabbit openaiFormat.ts:931 Biome lint/suspicious/noAssignInExpressions — sepIdx = ... inside while condition Refactored the event parsing into a shared drainBufferedEvents(controller) helper. The assignment is no longer inside the condition; sepIdx is computed before the loop and refreshed at the bottom of each iteration.
CR-D (decoder flush) CodeRabbit openaiFormat.ts:957 flush() did not drain the TextDecoder flush() now calls decoder.decode() to release any held bytes, then re-runs drainBufferedEvents so a complete trailing event that arrived without \n\n is not silently lost. Regression test added (transform receives an event split across two chunk boundaries).
CR-E (stream timeout) CodeRabbit proxyTranslationEngine.ts:363, 577 ctx.neurolink.stream() had no timeout in either path Both stream and JSON paths now wrap the call in withTimeout(..., TRANSLATION_ATTEMPT_TIMEOUT_MS) (5 minutes).
CR-F (tracer status) CodeRabbit proxyTranslationEngine.ts:488 Stream path hardcoded tracer.end(200, ...) even when all attempts failed Now tracer.end(succeeded ? 200 : 500, ...), matching the responseStatus already used in the paired logRequest() call.
CR-G (ErrorFactory) CodeRabbit proxyTranslationEngine.ts:682 throw new Error(lastAttemptError) violates project's typed-error convention Replaced with throw new NeuroLinkError({ code: ERROR_CODES.PROVIDER_NOT_AVAILABLE, category: EXECUTION, severity: HIGH, retriable: false, context: { attemptLabel, format } }). The outer catch in openaiProxyRoutes.ts already reads .message, so the wire contract is unchanged.
CR-H (anthropic /v1/models) CodeRabbit claudeProxyRoutes.ts:4604 Claude /v1/models returned OpenAI list format — breaks Anthropic SDK consumers Added buildAnthropicModelsListResponse(modelRouter) to proxyTranslationEngine.ts that emits the Anthropic schema ({ data: [{ type, id, display_name, created_at }], first_id, last_id, has_more }). The Claude route now calls this; the OpenAI route continues to use buildModelsListResponse for the OpenAI list shape.

Files modified

  • src/lib/server/routes/openaiProxyRoutes.ts — C1, C2, CR-A (timeout), CR-B (sanitize)
  • src/lib/proxy/openaiFormat.ts — CR-C (lint), CR-D (decoder flush)
  • src/lib/proxy/proxyTranslationEngine.ts — C3, C4, CR-E, CR-F, CR-G + new Anthropic models builder
  • src/lib/server/routes/claudeProxyRoutes.ts — CR-H
  • src/cli/commands/proxy.ts — C5, C6, plumbed loopbackPort to createOpenAIProxyRoutes
  • test/continuous-test-suite-proxy-openai-format.ts — new, 10 tests covering C1 + CR-D regressions and the core converters

Validation

  • TypeScript strict: ✅ 0 errors (npx tsc --noEmit --strict)
  • Lint: ✅ 0 errors, 15 pre-existing warnings (none in changed files)
  • New format test suite: ✅ 10/10 pass
  • Build: ✅ pnpm run build:cli
  • Single-commit policy: ✅ amended into 3c0c2eb4

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.

@coderabbitai

coderabbitai Bot commented May 24, 2026

Copy link
Copy Markdown

@murdore: Excellent, thorough resolution of every finding — appreciate the detailed accounting. A few notes on the cycle:

  • CR-B adaptation — using sanitizeForLog() over transformParamsForLogging() is the right call; truncation + secret-pattern redaction is exactly what's needed at those log sites. ✅
  • C6 persistence — mirroring Claude's __proxy_original_env pattern for the OpenCode snapshot (__proxy_original_neurolink inside opencode.json) is a solid crash-safe approach. ✅
  • CR-G — switching to NeuroLinkError with the typed error factory keeps the project's error taxonomy intact without changing the wire contract. ✅
  • CR-H — separating buildAnthropicModelsListResponse / buildModelsListResponse to emit the correct schema per route is a clean solution. ✅

Triggering a full re-review of commit 3c0c2eb4 now.

(^・ω・^)ノ

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

Comment thread src/lib/server/routes/openaiProxyRoutes.ts Fixed
@murdore

murdore commented May 24, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 24, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

…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.
@murdore
murdore force-pushed the feat/opencode-support-for-proxy branch from 3c0c2eb to f3bbc41 Compare May 24, 2026 20:23
@murdore

murdore commented May 24, 2026

Copy link
Copy Markdown
Contributor Author

Cycle 2 — CodeQL stack-trace exposure fix

Pushed f3bbc414. Single change in src/lib/server/routes/openaiProxyRoutes.ts:

Issue: CodeQL flagged information-exposure-through-stack-trace at openaiProxyRoutes.ts:61. Taint flow was catch(err) → err.message → sanitizeForLog() → response body. sanitizeForLog redacts secret patterns but doesn't strip stack-frame text, so internal paths could leak to the client.

Fix: The two catch blocks (loopback exception @ ~331, outer translation exception @ ~418) now return fixed generic messages to the client ("Anthropic loopback failed" / "Internal proxy error"). The sanitized exception detail is still preserved server-side via logger.always(), logRequest(), and tracer.setError() — so ops can still debug, but clients get a clean 502/500 envelope with no stack content.

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

  • TypeScript strict: ✅ 0 errors
  • Lint: ✅ 0 errors, 15 pre-existing warnings (none in changed file)
  • test/continuous-test-suite-proxy-openai-format.ts: ✅ 10/10 pass
  • pnpm run build:cli: ✅
  • Single-commit policy: ✅ amended into f3bbc414

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 24, 2026

Copy link
Copy Markdown

@murdore: The Cycle 2 fix is exactly the right pattern — returning fixed generic strings to the client ("Anthropic loopback failed" / "Internal proxy error") while keeping the sanitized exception detail entirely server-side breaks the taint flow CodeQL was tracking without sacrificing debuggability. The decision to leave lines 216 and 299 untouched is also correct: upstream-status passthrough reflects the upstream's own message (not a caught local exception), and the 400 message is static.

A few quick confirmations on the approach:

  • err.message → fixed string: eliminates the entire taint path from the caught exception to the response body. ✅
  • logger.always() + logRequest() + tracer.setError(): all still receive the sanitized detail, so nothing is lost from the ops side. ✅
  • Scope of the fix: limiting it to the two catch blocks that handle exceptions from internal code is precise — avoids over-sanitizing error messages that are legitimate to surface (e.g., upstream API errors). ✅

Triggering re-review of f3bbc414 now.

(^・ω・^)ノ

✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@github-actions

Copy link
Copy Markdown
Contributor

🤖 AI Review & Build Compliance ✅

Status: AI analysis complete • Build rules validated • Ready for review

📊 View detailed analysis results

🛡️ Analysis Complete

  • ✅ Security scan (vulnerabilities, API keys)
  • ✅ TypeScript safety & code quality
  • ✅ Error handling & best practices
  • ✅ Build rule enforcement validated
  • ✅ Commit format & compliance checks

📋 Ready for Merge When

  • All CI checks passing
  • Manual review approved
  • Any AI-flagged issues resolved

🤖 AI analysis complete - check individual code comments for specific feedback

@murdore

murdore commented May 24, 2026

Copy link
Copy Markdown
Contributor Author

All Reviewers Clean — Ready for Merge

Two fix cycles completed. Current commit: f3bbc414 (single squashed commit, 13 files, +4033/-473).

Verification snapshot

Signal Status
All 18 CI checks ✅ SUCCESS
Open code-scanning alerts on branch ✅ Zero
CodeRabbit inline findings since cycle 2 ✅ Zero (full re-review of f3bbc414 ran clean; positive acknowledgment of both cycles in issuecomment-4529835668 and issuecomment-4529868083 follow-up)
Copilot inline findings since cycle 2 ✅ Zero (Copilot PR Review CI check ran and produced no new comments on f3bbc414)
Single commit policy ✅ Compliant (1 commit, semantic format, ready for squash-merge)

Cycles summary

Cycle 1 — 3c0c2eb4 (summary): resolved 15 distinct findings across Copilot (7) + CodeRabbit (11) — error-status mapping, SSRF, log sanitization, thinking-config regression, model-ID typos, withTimeout wrapping on stream + loopback, tracer status, ErrorFactory adoption, decoder flush, Biome lint, dev-mode OpenCode gate, persisted OpenCode snapshot, Anthropic /v1/models schema, and 10 new automated tests for the OpenAI route.

Cycle 2 — f3bbc414 (summary): resolved CodeQL information-exposure-through-stack-trace alert. The two 5xx catch blocks in openaiProxyRoutes.ts now return fixed generic messages to the client ("Anthropic loopback failed" / "Internal proxy error"); sanitized exception detail stays in server logs + tracer only.

Files touched (final)

  • src/lib/server/routes/openaiProxyRoutes.ts (new)
  • src/lib/proxy/openaiFormat.ts (new)
  • src/lib/proxy/proxyTranslationEngine.ts (new)
  • src/lib/server/routes/claudeProxyRoutes.ts
  • src/lib/server/routes/index.ts
  • src/lib/types/proxy.ts
  • src/lib/types/server.ts
  • src/lib/proxy/modelRouter.ts
  • src/cli/commands/proxy.ts
  • docs/features/opencode-proxy-support.md (new)
  • test/continuous-test-suite-proxy-openai-format.ts (new — 10 unit tests)
  • test/fixtures/opencode-local-proxy.json (new)
  • test/fixtures/opencode-local-proxy-openai-route.json (new)

PR ready for merge.

@murdore
murdore merged commit b2df40e into release May 24, 2026
18 checks passed
@murdore
murdore deleted the feat/opencode-support-for-proxy branch May 24, 2026 20:46
@github-actions

Copy link
Copy Markdown
Contributor

🎉 This PR is included in version 9.66.0 🎉

The release is available on:

Your semantic-release bot 📦🚀

This branch was successfully deployed

1 active deployment
Preview — f3bbc414 Deployed May 24, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants