Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
- Added OpenAI-compatible proxy: profiles can now route Claude Code through any OpenAI-compatible API provider (OpenAI, Azure, Groq, DeepSeek, Ollama, local models, etc.)

# 1.1.7 (2026-05-22)

- Fixed profile combo not refreshing when profiles are added or removed after session start
Expand Down
5 changes: 4 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,8 @@ The plugin follows an MVC structure for session management:
| `ui/common/` | Shared input components: `AtCompletionPopup` — @-triggered path popup; `AtPathHighlighter` — blue token highlight; `FileDropHandler` — DnD + Ctrl+V → @path insertion; `ShortcutMatcher` — key→shortcut match with KEY_TYPED suppression; `TextComponentDecorator` — wires all the above; `DecoratedTextArea/TextField`, `TextContextMenu`, `RangeHighlightable` |
| `settings/` | `ClaudeCodePreferences` — NbPreferences wrapper (keys: `contextMenuSessionMode` default CONTINUE_LAST, `sessionListLimit` default 30); `ClaudeCodeOptionsPanelController` / `ClaudeCodeOptionsPanel` — Tools→Options (General + Profiles tabs); `ClaudeProfile`, `ClaudeProfileStore` — named profiles with isolated `CLAUDE_CONFIG_DIR`, auth, proxy, extra env vars; `ClaudeProjectProperties` — per-project profile assignment |
| `actions/` | `ClaudeCodeAction` — toolbar button; `OpenWithClaudeAction` — project node context menu; `PromptFavoriteAction` — dynamically registered action per favorite for Keymap API |
| `io.github.nbplugins.claudecodegui.mcp` | `MCPSseServer` — Jetty `/sse` `/messages` `/hook`; `NetBeansMCPHandler` — MCP dispatcher + PreToolUse hook; `tools/` — `OpenDiff`, `PermissionPromptTool`, `DiffTabTracker`, `GetDiagnostics`, `GetOpenEditors`, `GetCurrentSelection`, `OpenFile` |
| `io.github.nbplugins.claudecodegui.mcp` | `MCPSseServer` — Jetty `/sse` `/messages` `/hook` `/openai-proxy/*`; `NetBeansMCPHandler` — MCP dispatcher + PreToolUse hook; `tools/` — `OpenDiff`, `PermissionPromptTool`, `DiffTabTracker`, `GetDiagnostics`, `GetOpenEditors`, `GetCurrentSelection`, `OpenFile` |
| `io.github.nbplugins.claudecodegui.openaiproxy` | `OpenAIProxyConfig` — per-session config snapshot; `OpenAIProxyServlet` — translates Anthropic → OpenAI requests and responses; `AnthropicToOpenAITranslator` — stateless translation logic including streaming SSE state machine |
| `org.openbeans.claude.netbeans` | **Legacy package — do not add new code here and avoid modifying existing code.** Contains: `ClaudeCodeStatusService`, `ClaudeCodeStatusLineElement`, `EditorUtils`, `NbUtils`; `tools/` — `AsyncHandler`, `AsyncResponse`, `GetWorkspaceFolders`, `CheckDocumentDirty`, `SaveDocument`, `CloseTab`, `CloseAllDiffTabs` |

### Session lifecycle
Expand Down Expand Up @@ -255,6 +256,8 @@ After any action the originating `ClaudeSessionTab` is re-activated automaticall
| "Start new session when opening with Claude" checkbox | `contextMenuSessionMode` | `CONTINUE_LAST` | Checked → `SessionMode.NEW`; unchecked → `SessionMode.CONTINUE_LAST`. Controls the mode used by the toolbar button and project context menu. |
| "Session list limit" spinner (1–500) | `sessionListLimit` | 30 | Max number of past sessions shown in the resume list. |

**Note:** Changes to profile connection parameters (API key, base URL, proxy settings) take effect only after restarting the session. This applies to all connection types, including OpenAI Compatible Proxy.

### Registration
`layer.xml` registers the Options category (position 1500) and toolbar action. Icon PNGs are generated at build time from `cc-gui-icon.svg` via the Groovy script in `pom.xml` using Apache Batik.

Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ NetBeans Claude Code GUI is a NetBeans IDE plugin that embeds the Claude Code CL
- **Prompt history and favorites** — recall past prompts with Ctrl+Up/Down; save reusable prompts as favorites with optional keyboard shortcuts
- **File attachments** — attach files via `@path` tokens with auto-completion popup, drag-and-drop, and clipboard paste support
- **Multiple profiles** — run Claude Code under separate accounts or API keys for different projects, each with an isolated config directory, authentication, proxy, and model settings
- **OpenAI-compatible proxy** — route Claude Code through any OpenAI-compatible provider (OpenAI, Azure OpenAI, Groq, DeepSeek, Ollama, LM Studio, and others) via a built-in translation proxy; no changes to Claude Code CLI required
- **Session management** — start new sessions, continue the last session, or resume a specific past session; sessions persist across IDE restarts
- **Markdown Preview** — live-rendered markdown tab for plan files and MCP-initiated previews; includes a find bar (Ctrl+F) and font zoom (Alt+Scroll)
- **Auto Plan Preview** — when Claude writes a plan file, a live preview tab opens automatically as soon as you accept the diff
Expand Down
Binary file modified docs/screenshots/profiles-tab.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
60 changes: 60 additions & 0 deletions docs/testing-openai-proxy-opencode-zen.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# Testing OpenAI Compatible Proxy with OpenCode Zen

OpenCode Zen provides free OpenAI-compatible models that can be used to test the proxy feature without spending money.

## 1. Registration and API Key

1. Go to https://opencode.ai/auth
2. Sign in with Google or GitHub
3. Add billing details (a card is required even for free models — no charges for free-tier usage)
4. Copy your API key from the account dashboard

## 2. Plugin Profile Setup

Open **Tools → Options → Claude Code → Profiles**, create a new profile:

| Field | Value |
|---|---|
| Connection type | OpenAI Compatible Proxy |
| Base URL | `https://opencode.ai/zen` |
| API Key | your key from step 1 |

> **Note:** do not add `/v1` to the Base URL — the plugin appends it automatically when routing requests (`/v1/chat/completions`) and fetching models (`/v1/models`).

## 3. Free Models

Click **Model Aliases** in the profile editor. Use **Fetch** to load the current model list, or add manually:

| Model name | Model ID |
|---|---|
| Big Pickle | `opencode/big-pickle` |
| DeepSeek V4 Flash Free | `opencode/deepseek-v4-flash-free` |
| Nemotron 3 Super Free | `opencode/nemotron-3-super-free` |

All three models are $0 for input and output, context window 200K+. The list is time-limited while model teams collect feedback — check https://opencode.ai/docs/zen/ for the current list.

Map Claude model tiers to OpenCode models, for example:

| Alias (Claude tier) | Target model |
|---|---|
| `claude-sonnet-4-5` | `opencode/big-pickle` |
| `claude-haiku-4-5` | `opencode/deepseek-v4-flash-free` |
| `claude-opus-4-7` | `opencode/nemotron-3-super-free` |

Claude Code sends requests using names like `claude-sonnet-4-5` — Model Aliases redirects them to the actual provider model. Without an alias the request will fail because OpenCode does not recognize Claude model names.

## 4. Start a Session

Start a Claude Code session with this profile. The plugin will:
- Set `ANTHROPIC_BASE_URL` to the internal proxy (`http://127.0.0.1:<mcp-port>/openai-proxy/<uuid>`)
- Translate Anthropic Messages API requests → OpenAI Chat Completions
- Translate responses back, including streaming SSE and tool use

## 5. What to Verify

- **Basic conversation** — send a simple question, verify a response arrives
- **Streaming** — response should appear incrementally, not all at once
- **Tool use** — ask Claude to read or edit a file, verify the tool_use → tool_result round-trip works
- **Debug logging** — enable debug mode in preferences, then check `~/.netbeans/28/var/log/messages.log` for lines showing request/response metadata (headers, model, token counts) with no message content
- **Two simultaneous sessions** — open two sessions with the same profile, verify both work independently
- **Profile edit during session** — change the API key while a session is running, verify the running session is unaffected; restart to apply the new key
52 changes: 49 additions & 3 deletions docs/user-manual.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ Contains the input area, **▶ Send** / **✖ Cancel** buttons, and **☰ Histor

Press **Shift+Tab** anywhere in the prompt panel to cycle through modes.

**Model selector** — lists the models available in the current session. Standard entries (`sonnet`, `opus`, `haiku`) are discovered by opening the `/model` menu. For **Other API** profiles, any models assigned the `custom` alias in **Model Aliases** also appear here, shown by their full provider ID. Selecting a model switches to it immediately.
**Model selector** — lists the models available in the current session. Standard entries (`sonnet`, `opus`, `haiku`) are discovered by opening the `/model` menu. For **Claude-compatible API** profiles, any models assigned the `custom` alias in **Model Aliases** also appear here, shown by their full provider ID. Selecting a model switches to it immediately.

On the right side of the status bar:

Expand Down Expand Up @@ -487,9 +487,12 @@ Select the authentication type using the radio buttons:
| **Claude Managed** | None | Claude manages authentication itself. Run `claude` in a terminal once to set it up, or Claude will prompt you on first launch. Use `/login` in the session terminal to re-authenticate. |
| **Subscription** | OAuth token | Obtain the token at [claude.ai](https://claude.ai) (requires Pro, Max, Team, or Enterprise subscription). **Note:** OAuth tokens expire periodically and must be regenerated and re-entered manually. For a smoother experience, prefer **Claude Managed** instead. |
| **Claude API** | API key | Obtain the key at [console.anthropic.com](https://console.anthropic.com) (requires an Anthropic Console account with API access). |
| **Other API** | API key + Base URL | Use the API key and base URL provided by your third-party provider. |
| **Claude-compatible API** | API key + Base URL | Use the API key and base URL provided by your third-party provider (Anthropic-compatible endpoint). |
| **OpenAI-compatible API** | API key + Base URL | Route Claude Code through any OpenAI-compatible provider. The plugin starts an internal proxy that translates Anthropic API calls to OpenAI Chat Completions format. See [OpenAI-compatible API](#openai-compatible-api) below. |

### Model aliases (Other API only)
> **Note:** Changes to connection parameters (API key, Base URL) take effect only after restarting the session.

### Model aliases (Claude-compatible API only)

If your provider names models differently from Anthropic's standard names, the plugin cannot match them to the `sonnet`, `opus`, and `haiku` aliases used by Claude Code. In that case, set the alias to the actual model ID used by your provider (for example, with an `anthropic/` prefix).

Expand Down Expand Up @@ -546,6 +549,49 @@ The dialog shows a table with three columns:

**Note:** Claude Code communicates with the plugin via `localhost`. External proxies are not aware of this. If you fill in the **NO_PROXY** field, make sure it includes `localhost` — otherwise the plugin integration will stop working. If you leave the field empty, the plugin adds `localhost` automatically.

### OpenAI-compatible API

Claude Code sends requests in Anthropic Messages API format. Many AI providers — including OpenAI, Azure OpenAI, Groq, DeepSeek, and local models via Ollama or LM Studio — use OpenAI Chat Completions format instead. The **OpenAI-compatible API** connection type bridges this gap by running an internal HTTP proxy inside the plugin.

**How it works:**

1. When a session starts, the plugin registers an internal proxy path within the already-running MCP server.
2. Claude Code is directed to send requests to that path (`ANTHROPIC_BASE_URL=http://127.0.0.1:<mcp-port>/openai-proxy/<session-id>`).
3. The proxy translates each Anthropic Messages API request to OpenAI Chat Completions format, forwards it to your provider, and translates the response back — including streaming and tool calls.
4. When the session stops, the proxy path is deregistered. No separate port or process is used.

**Setup:**

1. Create a new profile and select **OpenAI-compatible API** as the connection type.
2. Fill in **Base URL** — the base URL of your provider's API (e.g. `https://api.openai.com/v1`, `http://localhost:11434/v1` for Ollama).
3. Fill in **API Key** — the API key for your provider (enter any non-blank value if the provider does not require a key).
4. Open **Model Aliases…** and map Claude model tiers to your provider's model names:

| Alias | Provider model example |
|-------|----------------------|
| `sonnet` | `gpt-4o` |
| `opus` | `gpt-4o` |
| `haiku` | `gpt-4o-mini` |

Without model aliases, Claude Code will send Anthropic model names (e.g. `claude-sonnet-4-5`) to your provider. Most OpenAI-compatible providers will reject these — configure aliases to avoid errors.

**Provider examples:**

| Provider | Base URL | Notes |
|----------|----------|-------|
| OpenAI | `https://api.openai.com/v1` | Standard OpenAI key |
| Azure OpenAI | `https://<resource>.openai.azure.com/openai/deployments/<deployment>` | Azure API key |
| Groq | `https://api.groq.com/openai/v1` | Groq API key |
| DeepSeek | `https://api.deepseek.com/v1` | DeepSeek API key |
| Ollama (local) | `http://localhost:11434/v1` | Any value for API key |
| LM Studio (local) | `http://localhost:1234/v1` | Any value for API key |

**Limitations:**

- Not all OpenAI-compatible models support all Claude Code features (tool use, large context windows). Using an incompatible model may cause errors.
- Images in conversations (base64 content) are forwarded in OpenAI vision format; provider must support it.
- Changes to the profile's Base URL or API Key take effect only after restarting the session.

### Extra environment variables

Arbitrary `KEY=VALUE` pairs injected into the Claude process environment. Applied last — they override all other profile variables. Use this for provider-specific configuration.
Expand Down
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

<groupId>io.github.nbplugins</groupId>
<artifactId>netbeans-plugin-claude-code-gui</artifactId>
<version>1.1.4-SNAPSHOT</version>
<version>1.2.7-SNAPSHOT</version>
<packaging>nbm</packaging>
<name>Netbeans Plugin Claude Code GUI</name>
<description>NetBeans plugin that provides a GUI for Claude Code CLI — run claude code sessions directly inside the IDE.</description>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,4 +247,24 @@ public int getServerPort() {
public boolean isLockFileValid() {
return false; // lock file no longer used
}

@Override
public void registerOpenAIProxy(String uuid, String baseUrl, String apiKey,
io.github.nbplugins.claudecodegui.settings.ProxyConfiguration proxy) {
if (mcpServer != null) {
mcpServer.registerOpenAIProxy(uuid, baseUrl, apiKey, proxy);
}
}

@Override
public void deregisterOpenAIProxy(String uuid) {
if (mcpServer != null) {
mcpServer.deregisterOpenAIProxy(uuid);
}
}

@Override
public io.github.nbplugins.claudecodegui.openaiproxy.OpenAIProxyConfig getOpenAIProxyConfig(String uuid) {
return mcpServer != null ? mcpServer.getOpenAIProxyConfig(uuid) : null;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.github.nbplugins.claudecodegui.openaiproxy.OpenAIProxyConfig;
import io.github.nbplugins.claudecodegui.openaiproxy.OpenAIProxyServlet;
import io.github.nbplugins.claudecodegui.settings.ProxyConfiguration;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.server.ServerConnector;
import org.eclipse.jetty.servlet.ServletContextHandler;
Expand Down Expand Up @@ -62,6 +65,13 @@ public class MCPSseServer {
private final ConcurrentHashMap<String, BlockingQueue<String>> sessionQueues =
new ConcurrentHashMap<>();

/**
* Registry of active OpenAI-compatible proxy sessions, keyed by UUID.
* Each entry is a config snapshot taken at session start.
*/
private final ConcurrentHashMap<String, OpenAIProxyConfig> openAIProxies =
new ConcurrentHashMap<>();

/**
* Creates a new MCPSseServer backed by the given MCP handler.
*
Expand Down Expand Up @@ -103,6 +113,7 @@ public boolean start(int port) {
ctx.addServlet(hookHolder, "/hook");
ctx.addServlet(new ServletHolder(new StopServlet()), "/stop");
ctx.addServlet(new ServletHolder(new PermissionRequestServlet()), "/permission-request");
ctx.addServlet(new ServletHolder(new OpenAIProxyServlet(this)), "/openai-proxy/*");

server.setHandler(ctx);
server.setRequestLog((request, response) ->
Expand Down Expand Up @@ -141,6 +152,43 @@ public void stop() {
*/
public int getPort() { return port; }

// -------------------------------------------------------------------------
// OpenAI proxy registry
// -------------------------------------------------------------------------

/**
* Registers an OpenAI-compatible proxy session.
*
* @param uuid unique session identifier
* @param baseUrl provider base URL
* @param apiKey provider API key
* @param proxy proxy settings from the profile (used by the servlet's HttpClient)
*/
public void registerOpenAIProxy(String uuid, String baseUrl, String apiKey, ProxyConfiguration proxy) {
openAIProxies.put(uuid, new OpenAIProxyConfig(baseUrl, apiKey, proxy));
LOGGER.log(Level.FINE, "OpenAI proxy registered: uuid={0}", uuid);
}

/**
* Deregisters an OpenAI-compatible proxy session.
*
* @param uuid unique session identifier
*/
public void deregisterOpenAIProxy(String uuid) {
openAIProxies.remove(uuid);
LOGGER.log(Level.FINE, "OpenAI proxy deregistered: uuid={0}", uuid);
}

/**
* Returns the proxy config for the given UUID, or {@code null} if not found.
*
* @param uuid unique session identifier
* @return config snapshot or {@code null}
*/
public OpenAIProxyConfig getOpenAIProxyConfig(String uuid) {
return openAIProxies.get(uuid);
}

/**
* Returns {@code true} while the Jetty server is running.
*
Expand Down
Loading
Loading