From 2b842ab1320769a0411767b0b6ab95574147777a Mon Sep 17 00:00:00 2001 From: Oleg Samarin Date: Fri, 22 May 2026 22:47:31 +0300 Subject: [PATCH 1/3] Added OpenAI-compatible proxy: profiles can now route Claude Code through any OpenAI-compatible API provider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - New OpenAI-compatible API connection type in profile settings - Internal HTTP proxy embedded in MCPSseServer (no new port) with per-session UUID routing - AnthropicToOpenAITranslator: full bidirectional translation including streaming SSE, tool use, thinking/reasoning_content blocks, and error normalization - Profile proxy settings (system/none/custom) applied to outbound OpenAI requests - Renamed Other API → Claude-compatible API, OpenAI Compatible Proxy → OpenAI-compatible API - 20 unit tests for AnthropicToOpenAITranslator with JSON fixtures - User manual, README, CLAUDE.md updated; testing guide added Co-Authored-By: Claude Sonnet 4.6 --- CHANGELOG.md | 2 + CLAUDE.md | 5 +- README.md | 1 + docs/screenshots/profiles-tab.png | Bin 84857 -> 77124 bytes docs/testing-openai-proxy-opencode-zen.md | 60 ++ docs/user-manual.md | 52 +- pom.xml | 2 +- .../claudecodegui/ClaudeCodeInstaller.java | 20 + .../claudecodegui/mcp/MCPSseServer.java | 48 ++ .../claudecodegui/process/ClaudeProcess.java | 38 +- .../proxy/AnthropicToOpenAITranslator.java | 723 ++++++++++++++++++ .../proxy/OpenAIProxyConfig.java | 41 + .../proxy/OpenAIProxyServlet.java | 274 +++++++ .../claudecodegui/settings/ClaudeProfile.java | 33 +- .../settings/ClaudeProfilesPanel.java | 38 +- .../settings/ProxyConfiguration.java | 77 ++ .../netbeans/ClaudeCodeStatusService.java | 32 +- .../AnthropicToOpenAITranslatorTest.java | 277 +++++++ .../proxy/req_max_tokens_claude.json | 5 + .../proxy/req_max_tokens_non_claude.json | 5 + .../proxy/req_mixed_assistant.json | 11 + .../claudecodegui/proxy/req_simple_text.json | 5 + .../proxy/req_system_as_blocks.json | 9 + .../proxy/req_system_prompt.json | 6 + .../proxy/req_thinking_assistant.json | 11 + .../proxy/req_tool_result_list_content.json | 12 + .../proxy/req_tool_result_user.json | 12 + .../proxy/req_tool_use_assistant.json | 13 + .../claudecodegui/proxy/req_tools.json | 13 + .../claudecodegui/proxy/resp_text.json | 8 + .../claudecodegui/proxy/resp_tool_calls.json | 19 + 31 files changed, 1833 insertions(+), 19 deletions(-) create mode 100644 docs/testing-openai-proxy-opencode-zen.md create mode 100644 src/main/java/io/github/nbplugins/claudecodegui/proxy/AnthropicToOpenAITranslator.java create mode 100644 src/main/java/io/github/nbplugins/claudecodegui/proxy/OpenAIProxyConfig.java create mode 100644 src/main/java/io/github/nbplugins/claudecodegui/proxy/OpenAIProxyServlet.java create mode 100644 src/main/java/io/github/nbplugins/claudecodegui/settings/ProxyConfiguration.java create mode 100644 src/test/java/io/github/nbplugins/claudecodegui/proxy/AnthropicToOpenAITranslatorTest.java create mode 100644 src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_max_tokens_claude.json create mode 100644 src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_max_tokens_non_claude.json create mode 100644 src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_mixed_assistant.json create mode 100644 src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_simple_text.json create mode 100644 src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_system_as_blocks.json create mode 100644 src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_system_prompt.json create mode 100644 src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_thinking_assistant.json create mode 100644 src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_tool_result_list_content.json create mode 100644 src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_tool_result_user.json create mode 100644 src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_tool_use_assistant.json create mode 100644 src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_tools.json create mode 100644 src/test/resources/io/github/nbplugins/claudecodegui/proxy/resp_text.json create mode 100644 src/test/resources/io/github/nbplugins/claudecodegui/proxy/resp_tool_calls.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 67ad9905..dff93854 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index 101fd4a8..d20986fd 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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.proxy` | `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 @@ -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. diff --git a/README.md b/README.md index 9d4dcf03..72061277 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/screenshots/profiles-tab.png b/docs/screenshots/profiles-tab.png index b80de9f570b42d7b49a6362118db5276ad8214d3..9bdabdf1707f0528e9360536402e27dba32b5161 100644 GIT binary patch literal 77124 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%0A_hSnk+rr9a?%2hY-Z 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 l4agmo6rYv **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:/openai-proxy/`) +- 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 diff --git a/docs/user-manual.md b/docs/user-manual.md index d1f61b82..8c6f9af4 100644 --- a/docs/user-manual.md +++ b/docs/user-manual.md @@ -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: @@ -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). @@ -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:/openai-proxy/`). +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://.openai.azure.com/openai/deployments/` | 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. diff --git a/pom.xml b/pom.xml index 23c24755..9cf54b96 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.nbplugins netbeans-plugin-claude-code-gui - 1.1.4-SNAPSHOT + 1.2.5-SNAPSHOT nbm Netbeans Plugin Claude Code GUI NetBeans plugin that provides a GUI for Claude Code CLI — run claude code sessions directly inside the IDE. diff --git a/src/main/java/io/github/nbplugins/claudecodegui/ClaudeCodeInstaller.java b/src/main/java/io/github/nbplugins/claudecodegui/ClaudeCodeInstaller.java index b75a4f56..4ca2650f 100644 --- a/src/main/java/io/github/nbplugins/claudecodegui/ClaudeCodeInstaller.java +++ b/src/main/java/io/github/nbplugins/claudecodegui/ClaudeCodeInstaller.java @@ -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.proxy.OpenAIProxyConfig getOpenAIProxyConfig(String uuid) { + return mcpServer != null ? mcpServer.getOpenAIProxyConfig(uuid) : null; + } } diff --git a/src/main/java/io/github/nbplugins/claudecodegui/mcp/MCPSseServer.java b/src/main/java/io/github/nbplugins/claudecodegui/mcp/MCPSseServer.java index f0ff0818..725fb036 100644 --- a/src/main/java/io/github/nbplugins/claudecodegui/mcp/MCPSseServer.java +++ b/src/main/java/io/github/nbplugins/claudecodegui/mcp/MCPSseServer.java @@ -4,6 +4,9 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; +import io.github.nbplugins.claudecodegui.proxy.OpenAIProxyConfig; +import io.github.nbplugins.claudecodegui.proxy.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; @@ -62,6 +65,13 @@ public class MCPSseServer { private final ConcurrentHashMap> 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 openAIProxies = + new ConcurrentHashMap<>(); + /** * Creates a new MCPSseServer backed by the given MCP handler. * @@ -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) -> @@ -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. * diff --git a/src/main/java/io/github/nbplugins/claudecodegui/process/ClaudeProcess.java b/src/main/java/io/github/nbplugins/claudecodegui/process/ClaudeProcess.java index 9b917b1e..92f0bea4 100644 --- a/src/main/java/io/github/nbplugins/claudecodegui/process/ClaudeProcess.java +++ b/src/main/java/io/github/nbplugins/claudecodegui/process/ClaudeProcess.java @@ -20,6 +20,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.logging.Logger; import org.openbeans.claude.netbeans.ClaudeCodeStatusService; @@ -78,6 +79,9 @@ public ClaudeProcess() {} /** Temp file holding the --mcp-config JSON; {@code null} when not in use. */ private volatile Path mcpConfigTempFile; + /** UUID of the active OpenAI proxy session; {@code null} when not using OpenAI proxy. */ + private volatile String openAIProxyUuid; + /** * Starts a Claude CLI PTY process in the given working directory using * the Default profile (no extra env vars injected). @@ -155,7 +159,28 @@ public PtyProcess start(String workingDir, ClaudeProfile profile, String extraCl Map env = buildEnv(profile, ClaudeCodePreferences.getProfilesDir()); - boolean apiKeyHelper = profile != null && !profile.getApiKey().isBlank() && profile.getBaseUrl().isBlank(); + // OpenAI proxy: register session and inject ANTHROPIC_BASE_URL pointing to internal proxy + if (profile != null + && profile.computeConnectionType() == ClaudeProfile.ConnectionType.OPENAI_PROXY) { + ClaudeCodeStatusService mcpSvc = Lookup.getDefault().lookup(ClaudeCodeStatusService.class); + if (mcpSvc != null && mcpSvc.isServerRunning()) { + String uuid = UUID.randomUUID().toString(); + mcpSvc.registerOpenAIProxy(uuid, profile.getBaseUrl(), profile.getApiKey(), + io.github.nbplugins.claudecodegui.settings.ProxyConfiguration.from(profile)); + openAIProxyUuid = uuid; + env.put("ANTHROPIC_BASE_URL", + "http://127.0.0.1:" + mcpSvc.getServerPort() + "/openai-proxy/" + uuid); + env.put("ANTHROPIC_AUTH_TOKEN", "sk-proxy-internal"); + LOG.info("OpenAI proxy registered: uuid=" + uuid + + ", profile=" + profile.getName() + + ", target=" + profile.getBaseUrl()); + } else { + LOG.warning("OpenAI proxy: MCP server not running — proxy cannot be started"); + } + } + + boolean apiKeyHelper = profile != null && !profile.getApiKey().isBlank() && profile.getBaseUrl().isBlank() + && profile.computeConnectionType() != ClaudeProfile.ConnectionType.OPENAI_PROXY; LOG.info("Starting Claude: profile=" + (profile != null ? profile.getName() + " (" + profile.computeConnectionType() + ")" : "Default") + ", apiKeyHelper=" + (apiKeyHelper ? "SET" : "NOT SET") + ", ANTHROPIC_AUTH_TOKEN=" + (!env.getOrDefault("ANTHROPIC_AUTH_TOKEN", "").isBlank() ? "SET" : "NOT SET") @@ -313,6 +338,17 @@ public void killOnly() { public void stop() { killOnly(); + // Deregister OpenAI proxy session if active + String proxyUuid = openAIProxyUuid; + openAIProxyUuid = null; + if (proxyUuid != null) { + ClaudeCodeStatusService mcpSvc = Lookup.getDefault().lookup(ClaudeCodeStatusService.class); + if (mcpSvc != null) { + mcpSvc.deregisterOpenAIProxy(proxyUuid); + LOG.fine("OpenAI proxy deregistered: uuid=" + proxyUuid); + } + } + Path tmp = mcpConfigTempFile; mcpConfigTempFile = null; if (tmp != null) { diff --git a/src/main/java/io/github/nbplugins/claudecodegui/proxy/AnthropicToOpenAITranslator.java b/src/main/java/io/github/nbplugins/claudecodegui/proxy/AnthropicToOpenAITranslator.java new file mode 100644 index 00000000..27a369c2 --- /dev/null +++ b/src/main/java/io/github/nbplugins/claudecodegui/proxy/AnthropicToOpenAITranslator.java @@ -0,0 +1,723 @@ +package io.github.nbplugins.claudecodegui.proxy; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.util.Iterator; +import java.util.Map; + +/** + * Stateless translator between Anthropic Messages API format and + * OpenAI Chat Completions API format. + * + *

Based on open-source reference implementations: + *

+ * + *

Request (Anthropic → OpenAI)

+ *
    + *
  • {@code system} field → prepended as {@code {"role":"system",...}} message
  • + *
  • User text content blocks → flattened to string
  • + *
  • User tool_result blocks → converted to {@code role:"tool"} messages
  • + *
  • Assistant tool_use blocks → converted to {@code tool_calls}
  • + *
  • Image blocks → converted to OpenAI vision format
  • + *
  • {@code tools} → wrapped in {@code {"type":"function","function":{...}}}
  • + *
  • {@code max_tokens} → capped at 16384 for non-Claude models
  • + *
+ * + *

Response non-streaming (OpenAI → Anthropic)

+ * Converts {@code choices[0].message} back to Anthropic content blocks. + * + *

Response streaming

+ * State machine converting OpenAI SSE chunks to Anthropic SSE event sequence. + */ +public final class AnthropicToOpenAITranslator { + + static final ObjectMapper MAPPER = new ObjectMapper(); + + private static final int MAX_TOKENS_NON_CLAUDE = 16384; + + private AnthropicToOpenAITranslator() {} + + // ------------------------------------------------------------------------- + // Request translation + // ------------------------------------------------------------------------- + + /** + * Translates an Anthropic {@code POST /v1/messages} request body to an + * OpenAI {@code POST /chat/completions} request body. + * + * @param anthropicRequest parsed Anthropic request JSON + * @return OpenAI-format request JSON + */ + public static ObjectNode translateRequest(JsonNode anthropicRequest) { + ObjectNode openai = MAPPER.createObjectNode(); + + // Model — pass as-is; user configures ANTHROPIC_DEFAULT_*_MODEL aliases + String model = anthropicRequest.path("model").asText(""); + openai.put("model", model); + + // max_tokens + if (anthropicRequest.has("max_tokens")) { + int maxTokens = anthropicRequest.get("max_tokens").asInt(); + if (!model.startsWith("claude")) { + maxTokens = Math.min(maxTokens, MAX_TOKENS_NON_CLAUDE); + } + openai.put("max_tokens", maxTokens); + } + + // stream + if (anthropicRequest.has("stream")) { + openai.put("stream", anthropicRequest.get("stream").asBoolean()); + } + + // temperature, top_p (pass through if present) + for (String field : new String[]{"temperature", "top_p"}) { + if (anthropicRequest.has(field)) { + openai.set(field, anthropicRequest.get(field)); + } + } + + // Messages + ArrayNode messages = openai.putArray("messages"); + + // System prompt → first message + JsonNode systemNode = anthropicRequest.path("system"); + if (!systemNode.isMissingNode() && !systemNode.isNull()) { + String systemText = extractSystemText(systemNode); + if (!systemText.isBlank()) { + ObjectNode sysMsg = messages.addObject(); + sysMsg.put("role", "system"); + sysMsg.put("content", systemText); + } + } + + // Anthropic messages + JsonNode anthropicMessages = anthropicRequest.path("messages"); + if (anthropicMessages.isArray()) { + convertMessages(anthropicMessages, messages); + } + + // Tools + JsonNode tools = anthropicRequest.path("tools"); + if (tools.isArray() && !tools.isEmpty()) { + ArrayNode openaiTools = openai.putArray("tools"); + convertTools(tools, openaiTools); + } + + // tool_choice + JsonNode toolChoice = anthropicRequest.path("tool_choice"); + if (!toolChoice.isMissingNode() && !toolChoice.isNull()) { + openai.set("tool_choice", convertToolChoice(toolChoice)); + } + + return openai; + } + + private static String extractSystemText(JsonNode systemNode) { + if (systemNode.isTextual()) { + return systemNode.asText(); + } + if (systemNode.isArray()) { + StringBuilder sb = new StringBuilder(); + for (JsonNode block : systemNode) { + if ("text".equals(block.path("type").asText())) { + if (sb.length() > 0) sb.append("\n\n"); + sb.append(block.path("text").asText()); + } + } + return sb.toString(); + } + return ""; + } + + private static void convertMessages(JsonNode anthropicMessages, ArrayNode openaiMessages) { + for (JsonNode msg : anthropicMessages) { + String role = msg.path("role").asText(); + JsonNode content = msg.path("content"); + + if ("user".equals(role)) { + convertUserMessage(content, openaiMessages); + } else if ("assistant".equals(role)) { + convertAssistantMessage(content, openaiMessages); + } + } + } + + private static void convertUserMessage(JsonNode content, ArrayNode out) { + if (content.isTextual()) { + ObjectNode msg = out.addObject(); + msg.put("role", "user"); + msg.put("content", content.asText()); + return; + } + + if (!content.isArray()) return; + + // Check if this is purely tool_result blocks — they become separate tool messages + boolean allToolResults = true; + for (JsonNode block : content) { + if (!"tool_result".equals(block.path("type").asText())) { + allToolResults = false; + break; + } + } + + if (allToolResults) { + for (JsonNode block : content) { + ObjectNode toolMsg = out.addObject(); + toolMsg.put("role", "tool"); + toolMsg.put("tool_call_id", block.path("tool_use_id").asText()); + toolMsg.put("content", extractToolResultContent(block.path("content"))); + } + return; + } + + // Mixed user content — check for images + boolean hasImage = false; + for (JsonNode block : content) { + if ("image".equals(block.path("type").asText())) { + hasImage = true; + break; + } + } + + if (!hasImage) { + // Text only — flatten to string + StringBuilder sb = new StringBuilder(); + for (JsonNode block : content) { + if ("text".equals(block.path("type").asText())) { + sb.append(block.path("text").asText()); + } + } + ObjectNode msg = out.addObject(); + msg.put("role", "user"); + msg.put("content", sb.toString()); + } else { + // Multimodal + ObjectNode msg = out.addObject(); + msg.put("role", "user"); + ArrayNode parts = msg.putArray("content"); + for (JsonNode block : content) { + String type = block.path("type").asText(); + if ("text".equals(type)) { + ObjectNode part = parts.addObject(); + part.put("type", "text"); + part.put("text", block.path("text").asText()); + } else if ("image".equals(type)) { + JsonNode src = block.path("source"); + String mediaType = src.path("media_type").asText("image/jpeg"); + String data = src.path("data").asText(); + ObjectNode part = parts.addObject(); + part.put("type", "image_url"); + part.putObject("image_url").put("url", "data:" + mediaType + ";base64," + data); + } + } + } + } + + private static String extractToolResultContent(JsonNode contentNode) { + if (contentNode.isTextual()) return contentNode.asText(); + if (contentNode.isNull() || contentNode.isMissingNode()) return ""; + if (contentNode.isArray()) { + StringBuilder sb = new StringBuilder(); + for (JsonNode block : contentNode) { + if ("text".equals(block.path("type").asText())) { + sb.append(block.path("text").asText()); + } + } + if (sb.length() > 0) return sb.toString(); + } + // Fallback: JSON serialize + return contentNode.toString(); + } + + private static void convertAssistantMessage(JsonNode content, ArrayNode out) { + if (content.isTextual()) { + ObjectNode msg = out.addObject(); + msg.put("role", "assistant"); + msg.put("content", content.asText()); + return; + } + + if (!content.isArray()) return; + + StringBuilder textBuffer = new StringBuilder(); + StringBuilder reasoningBuffer = new StringBuilder(); + ArrayNode toolCalls = null; + + for (JsonNode block : content) { + String type = block.path("type").asText(); + if ("text".equals(type)) { + textBuffer.append(block.path("text").asText()); + } else if ("thinking".equals(type)) { + reasoningBuffer.append(block.path("thinking").asText()); + } else if ("tool_use".equals(type)) { + if (toolCalls == null) { + toolCalls = MAPPER.createArrayNode(); + } + ObjectNode tc = toolCalls.addObject(); + tc.put("id", block.path("id").asText()); + tc.put("type", "function"); + ObjectNode fn = tc.putObject("function"); + fn.put("name", block.path("name").asText()); + JsonNode input = block.path("input"); + fn.put("arguments", input.isObject() ? input.toString() : "{}"); + } + } + + ObjectNode msg = out.addObject(); + msg.put("role", "assistant"); + if (reasoningBuffer.length() > 0) { + msg.put("reasoning_content", reasoningBuffer.toString()); + } + if (textBuffer.length() > 0) { + msg.put("content", textBuffer.toString()); + } else { + msg.putNull("content"); + } + if (toolCalls != null) { + msg.set("tool_calls", toolCalls); + } + } + + private static void convertTools(JsonNode anthropicTools, ArrayNode openaiTools) { + for (JsonNode tool : anthropicTools) { + ObjectNode oTool = openaiTools.addObject(); + oTool.put("type", "function"); + ObjectNode fn = oTool.putObject("function"); + fn.put("name", tool.path("name").asText()); + if (tool.has("description")) { + fn.put("description", tool.path("description").asText()); + } + JsonNode schema = tool.path("input_schema"); + if (!schema.isMissingNode()) { + fn.set("parameters", schema); + } + } + } + + private static JsonNode convertToolChoice(JsonNode toolChoice) { + if (toolChoice.isTextual()) { + String val = toolChoice.asText(); + return switch (val) { + case "any" -> MAPPER.getNodeFactory().textNode("required"); + case "auto" -> MAPPER.getNodeFactory().textNode("auto"); + case "none" -> MAPPER.getNodeFactory().textNode("none"); + default -> MAPPER.getNodeFactory().textNode("auto"); + }; + } + if (toolChoice.isObject()) { + String type = toolChoice.path("type").asText(); + if ("tool".equals(type)) { + ObjectNode choice = MAPPER.createObjectNode(); + choice.put("type", "function"); + choice.putObject("function").put("name", toolChoice.path("name").asText()); + return choice; + } + } + return MAPPER.getNodeFactory().textNode("auto"); + } + + // ------------------------------------------------------------------------- + // Response translation (non-streaming) + // ------------------------------------------------------------------------- + + /** + * Translates an OpenAI Chat Completions response to Anthropic Messages response format. + * + * @param openaiResponse parsed OpenAI response JSON + * @param model model name to include in response + * @return Anthropic-format response JSON + */ + public static ObjectNode translateResponse(JsonNode openaiResponse, String model) { + ObjectNode anthropic = MAPPER.createObjectNode(); + + anthropic.put("id", openaiResponse.path("id").asText("msg_proxy")); + anthropic.put("type", "message"); + anthropic.put("role", "assistant"); + anthropic.put("model", model); + + ArrayNode content = anthropic.putArray("content"); + JsonNode choices = openaiResponse.path("choices"); + String stopReason = "end_turn"; + + if (choices.isArray() && !choices.isEmpty()) { + JsonNode choice = choices.get(0); + JsonNode message = choice.path("message"); + + // Thinking / reasoning content (DeepSeek-style providers) + JsonNode rc = message.path("reasoning_content"); + if (!rc.isNull() && !rc.isMissingNode() && !rc.asText().isBlank()) { + ObjectNode tb = content.addObject(); + tb.put("type", "thinking"); + tb.put("thinking", rc.asText()); + tb.put("signature", "proxy_thinking_sig"); + } + + // Text content + JsonNode textContent = message.path("content"); + if (!textContent.isNull() && !textContent.isMissingNode()) { + String text = textContent.asText(); + if (!text.isBlank()) { + ObjectNode textBlock = content.addObject(); + textBlock.put("type", "text"); + textBlock.put("text", text); + } + } + + // Tool calls + JsonNode toolCalls = message.path("tool_calls"); + if (toolCalls.isArray()) { + for (JsonNode tc : toolCalls) { + ObjectNode toolUse = content.addObject(); + toolUse.put("type", "tool_use"); + toolUse.put("id", tc.path("id").asText()); + toolUse.put("name", tc.path("function").path("name").asText()); + String argsStr = tc.path("function").path("arguments").asText("{}"); + try { + toolUse.set("input", MAPPER.readTree(argsStr)); + } catch (Exception e) { + toolUse.putObject("input"); + } + } + } + + // finish_reason + stopReason = mapFinishReason(choice.path("finish_reason").asText("stop")); + } + + anthropic.put("stop_reason", stopReason); + anthropic.putNull("stop_sequence"); + + // Usage + ObjectNode usage = anthropic.putObject("usage"); + JsonNode openaiUsage = openaiResponse.path("usage"); + usage.put("input_tokens", openaiUsage.path("prompt_tokens").asInt(0)); + usage.put("output_tokens", openaiUsage.path("completion_tokens").asInt(0)); + + return anthropic; + } + + static String mapFinishReason(String openaiReason) { + return switch (openaiReason) { + case "length" -> "max_tokens"; + case "tool_calls", + "function_call" -> "tool_use"; + default -> "end_turn"; + }; + } + + // ------------------------------------------------------------------------- + // Streaming state machine + // ------------------------------------------------------------------------- + + /** + * Mutable per-request state for the streaming SSE translator. + * Create one instance per streaming request. + */ + public static final class StreamingState { + + private boolean messageStarted = false; + private boolean thinkingBlockOpen = false; + private int thinkingBlockIndex = -1; + private boolean textBlockOpen = false; + private int textBlockIndex = -1; + private int nextBlockIndex = 0; + private String stopReason = "end_turn"; + private int outputTokens = 0; + private int inputTokens = 0; + private String messageId = "msg_proxy"; + private String model = ""; + + // Per OpenAI tool_call index: {id, name, started} + private final java.util.Map toolCalls = new java.util.LinkedHashMap<>(); + + private static final class ToolCallState { + String id; + String name; + boolean started; + int blockIndex; + } + + /** + * Process one OpenAI SSE data line and return zero or more Anthropic SSE events. + * + * @param dataLine the raw data line (without the {@code "data: "} prefix) + * @return string of Anthropic SSE events to write to the client, may be empty + */ + public String processChunk(String dataLine) { + if ("[DONE]".equals(dataLine)) { + return buildDoneEvents(); + } + + JsonNode chunk; + try { + chunk = MAPPER.readTree(dataLine); + } catch (Exception e) { + return ""; + } + + StringBuilder out = new StringBuilder(); + + if (!messageStarted) { + messageId = chunk.path("id").asText("msg_proxy"); + model = chunk.path("model").asText(""); + out.append(sseEvent("message_start", buildMessageStart())); + out.append(sseEvent("ping", "{\"type\":\"ping\"}")); + messageStarted = true; + } + + JsonNode choices = chunk.path("choices"); + if (!choices.isArray() || choices.isEmpty()) { + // Might be a usage-only chunk + JsonNode usage = chunk.path("usage"); + if (!usage.isMissingNode()) { + outputTokens = usage.path("completion_tokens").asInt(outputTokens); + inputTokens = usage.path("prompt_tokens").asInt(inputTokens); + } + return out.toString(); + } + + JsonNode choice = choices.get(0); + JsonNode delta = choice.path("delta"); + + // Track finish_reason + String fr = choice.path("finish_reason").asText(null); + if (fr != null && !"null".equals(fr)) { + stopReason = mapFinishReason(fr); + } + + // Usage in this chunk (some providers send it here) + JsonNode usage = chunk.path("usage"); + if (!usage.isMissingNode()) { + outputTokens = usage.path("completion_tokens").asInt(outputTokens); + inputTokens = usage.path("prompt_tokens").asInt(inputTokens); + } + + // Reasoning (thinking) delta — DeepSeek/OpenCode style + JsonNode reasoningNode = delta.path("reasoning_content"); + if (!reasoningNode.isNull() && !reasoningNode.isMissingNode()) { + String rc = reasoningNode.asText(); + if (!rc.isEmpty()) { + if (!thinkingBlockOpen) { + thinkingBlockIndex = nextBlockIndex++; + out.append(sseEvent("content_block_start", + buildThinkingBlockStart(thinkingBlockIndex))); + thinkingBlockOpen = true; + } + out.append(sseEvent("content_block_delta", + buildThinkingDelta(thinkingBlockIndex, rc))); + } + } + + // Text delta + JsonNode contentNode = delta.path("content"); + if (!contentNode.isNull() && !contentNode.isMissingNode()) { + String text = contentNode.asText(); + if (!text.isEmpty()) { + // Close thinking block before opening text block + if (thinkingBlockOpen) { + out.append(sseEvent("content_block_delta", + buildSignatureDelta(thinkingBlockIndex))); + out.append(sseEvent("content_block_stop", + "{\"type\":\"content_block_stop\",\"index\":" + thinkingBlockIndex + "}")); + thinkingBlockOpen = false; + } + if (!textBlockOpen) { + textBlockIndex = nextBlockIndex++; + out.append(sseEvent("content_block_start", + buildTextBlockStart(textBlockIndex))); + textBlockOpen = true; + } + out.append(sseEvent("content_block_delta", + buildTextDelta(textBlockIndex, text))); + } + } + + // Tool call deltas + JsonNode toolCallsNode = delta.path("tool_calls"); + if (toolCallsNode.isArray()) { + for (JsonNode tc : toolCallsNode) { + int tcIdx = tc.path("index").asInt(0); + ToolCallState state = toolCalls.computeIfAbsent(tcIdx, k -> new ToolCallState()); + + String tcId = tc.path("id").asText(null); + String tcName = tc.path("function").path("name").asText(null); + if (tcId != null) state.id = tcId; + if (tcName != null) state.name = tcName; + + if (!state.started && state.id != null && state.name != null) { + // Close thinking block if open + if (thinkingBlockOpen) { + out.append(sseEvent("content_block_delta", + buildSignatureDelta(thinkingBlockIndex))); + out.append(sseEvent("content_block_stop", + "{\"type\":\"content_block_stop\",\"index\":" + thinkingBlockIndex + "}")); + thinkingBlockOpen = false; + } + // Close text block if open + if (textBlockOpen) { + out.append(sseEvent("content_block_stop", + "{\"type\":\"content_block_stop\",\"index\":" + textBlockIndex + "}")); + textBlockOpen = false; + } + state.blockIndex = nextBlockIndex++; + out.append(sseEvent("content_block_start", + buildToolUseBlockStart(state.blockIndex, state.id, state.name))); + state.started = true; + } + + String argsFrag = tc.path("function").path("arguments").asText(null); + if (argsFrag != null && !argsFrag.isEmpty() && state.started) { + out.append(sseEvent("content_block_delta", + buildInputJsonDelta(state.blockIndex, argsFrag))); + } + } + } + + return out.toString(); + } + + private String buildDoneEvents() { + StringBuilder out = new StringBuilder(); + if (thinkingBlockOpen) { + out.append(sseEvent("content_block_delta", buildSignatureDelta(thinkingBlockIndex))); + out.append(sseEvent("content_block_stop", + "{\"type\":\"content_block_stop\",\"index\":" + thinkingBlockIndex + "}")); + } + if (textBlockOpen) { + out.append(sseEvent("content_block_stop", + "{\"type\":\"content_block_stop\",\"index\":" + textBlockIndex + "}")); + } + for (ToolCallState state : toolCalls.values()) { + if (state.started) { + out.append(sseEvent("content_block_stop", + "{\"type\":\"content_block_stop\",\"index\":" + state.blockIndex + "}")); + } + } + out.append(sseEvent("message_delta", buildMessageDelta())); + out.append(sseEvent("message_stop", "{\"type\":\"message_stop\"}")); + return out.toString(); + } + + private String buildMessageStart() { + return String.format( + "{\"type\":\"message_start\",\"message\":{\"id\":\"%s\",\"type\":\"message\"," + + "\"role\":\"assistant\",\"content\":[],\"model\":\"%s\"," + + "\"stop_reason\":null,\"stop_sequence\":null," + + "\"usage\":{\"input_tokens\":%d,\"output_tokens\":0}}}", + messageId, model, inputTokens); + } + + private static String buildTextBlockStart(int index) { + return String.format( + "{\"type\":\"content_block_start\",\"index\":%d," + + "\"content_block\":{\"type\":\"text\",\"text\":\"\"}}", + index); + } + + private static String buildTextDelta(int index, String text) { + String escaped = text.replace("\\", "\\\\").replace("\"", "\\\"") + .replace("\n", "\\n").replace("\r", "\\r") + .replace("\t", "\\t"); + return String.format( + "{\"type\":\"content_block_delta\",\"index\":%d," + + "\"delta\":{\"type\":\"text_delta\",\"text\":\"%s\"}}", + index, escaped); + } + + private static String buildToolUseBlockStart(int index, String id, String name) { + return String.format( + "{\"type\":\"content_block_start\",\"index\":%d," + + "\"content_block\":{\"type\":\"tool_use\",\"id\":\"%s\",\"name\":\"%s\",\"input\":{}}}", + index, id, name); + } + + private static String buildInputJsonDelta(int index, String partialJson) { + String escaped = partialJson.replace("\\", "\\\\").replace("\"", "\\\"") + .replace("\n", "\\n").replace("\r", "\\r"); + return String.format( + "{\"type\":\"content_block_delta\",\"index\":%d," + + "\"delta\":{\"type\":\"input_json_delta\",\"partial_json\":\"%s\"}}", + index, escaped); + } + + private static String buildThinkingBlockStart(int index) { + return String.format( + "{\"type\":\"content_block_start\",\"index\":%d," + + "\"content_block\":{\"type\":\"thinking\",\"thinking\":\"\"}}", + index); + } + + private static String buildThinkingDelta(int index, String thinking) { + String escaped = thinking.replace("\\", "\\\\").replace("\"", "\\\"") + .replace("\n", "\\n").replace("\r", "\\r") + .replace("\t", "\\t"); + return String.format( + "{\"type\":\"content_block_delta\",\"index\":%d," + + "\"delta\":{\"type\":\"thinking_delta\",\"thinking\":\"%s\"}}", + index, escaped); + } + + private static String buildSignatureDelta(int index) { + return String.format( + "{\"type\":\"content_block_delta\",\"index\":%d," + + "\"delta\":{\"type\":\"signature_delta\",\"signature\":\"proxy_thinking_sig\"}}", + index); + } + + private String buildMessageDelta() { + return String.format( + "{\"type\":\"message_delta\",\"delta\":{\"stop_reason\":\"%s\"," + + "\"stop_sequence\":null},\"usage\":{\"output_tokens\":%d}}", + stopReason, outputTokens); + } + } + + // ------------------------------------------------------------------------- + // SSE helpers + // ------------------------------------------------------------------------- + + static String sseEvent(String eventType, String dataJson) { + return "event: " + eventType + "\ndata: " + dataJson + "\n\n"; + } + + // ------------------------------------------------------------------------- + // Debug summary helpers (no conversation content) + // ------------------------------------------------------------------------- + + /** Returns a one-line summary of an Anthropic request for debug logging. */ + public static String summarizeAnthropicRequest(JsonNode req) { + int msgCount = req.path("messages").size(); + int toolCount = req.path("tools").size(); + return "model=" + req.path("model").asText("?") + + " stream=" + req.path("stream").asBoolean(false) + + " max_tokens=" + req.path("max_tokens").asInt(0) + + " messages=" + msgCount + + " tools=" + toolCount; + } + + /** Returns a one-line summary of an OpenAI request for debug logging. */ + public static String summarizeOpenAIRequest(JsonNode req) { + int msgCount = req.path("messages").size(); + int toolCount = req.path("tools").size(); + return "model=" + req.path("model").asText("?") + + " stream=" + req.path("stream").asBoolean(false) + + " max_tokens=" + req.path("max_tokens").asInt(0) + + " messages=" + msgCount + + " tools=" + toolCount; + } + + /** Returns a one-line summary of an OpenAI non-streaming response for debug logging. */ + public static String summarizeOpenAIResponse(JsonNode resp) { + JsonNode usage = resp.path("usage"); + JsonNode choice = resp.path("choices").path(0); + return "finish_reason=" + choice.path("finish_reason").asText("?") + + " prompt_tokens=" + usage.path("prompt_tokens").asInt(0) + + " completion_tokens=" + usage.path("completion_tokens").asInt(0); + } +} diff --git a/src/main/java/io/github/nbplugins/claudecodegui/proxy/OpenAIProxyConfig.java b/src/main/java/io/github/nbplugins/claudecodegui/proxy/OpenAIProxyConfig.java new file mode 100644 index 00000000..c659a7f1 --- /dev/null +++ b/src/main/java/io/github/nbplugins/claudecodegui/proxy/OpenAIProxyConfig.java @@ -0,0 +1,41 @@ +package io.github.nbplugins.claudecodegui.proxy; + +import io.github.nbplugins.claudecodegui.settings.ProxyConfiguration; +import java.net.http.HttpClient; +import java.time.Duration; + +/** + * Snapshot of OpenAI-compatible endpoint configuration taken at session start. + * + *

Immutable after construction. Passed to {@link OpenAIProxyServlet} via + * {@link io.github.nbplugins.claudecodegui.mcp.MCPSseServer} registry. + * Changes to the profile after session start do not affect a running session. + */ +public final class OpenAIProxyConfig { + + private final String baseUrl; + private final String apiKey; + private final ProxyConfiguration proxy; + + public OpenAIProxyConfig(String baseUrl, String apiKey, ProxyConfiguration proxy) { + this.baseUrl = baseUrl; + this.apiKey = apiKey; + this.proxy = proxy; + } + + /** Base URL of the OpenAI-compatible endpoint (e.g. {@code https://api.openai.com/v1}). */ + public String getBaseUrl() { return baseUrl; } + + /** API key sent as {@code Authorization: Bearer }. */ + public String getApiKey() { return apiKey; } + + /** + * Builds an {@link HttpClient} configured with the proxy settings from this config. + * See {@link ProxyConfiguration#applyTo} for details. + */ + public HttpClient buildHttpClient() { + return proxy.applyTo( + HttpClient.newBuilder().connectTimeout(Duration.ofSeconds(30)) + ).build(); + } +} diff --git a/src/main/java/io/github/nbplugins/claudecodegui/proxy/OpenAIProxyServlet.java b/src/main/java/io/github/nbplugins/claudecodegui/proxy/OpenAIProxyServlet.java new file mode 100644 index 00000000..2a37fffd --- /dev/null +++ b/src/main/java/io/github/nbplugins/claudecodegui/proxy/OpenAIProxyServlet.java @@ -0,0 +1,274 @@ +package io.github.nbplugins.claudecodegui.proxy; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.github.nbplugins.claudecodegui.mcp.MCPSseServer; +import io.github.nbplugins.claudecodegui.settings.ClaudeCodePreferences; +import jakarta.servlet.http.HttpServlet; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.PrintWriter; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.Enumeration; +import java.util.logging.Logger; + +// Duration used in HttpRequest timeout + +/** + * Servlet registered at {@code /openai-proxy/*} that translates + * Anthropic Messages API requests to OpenAI Chat Completions format + * and translates responses back. + * + *

The URL structure is: + *

  /openai-proxy/{uuid}/v1/messages
+ * where {@code uuid} identifies the session and maps to an + * {@link OpenAIProxyConfig} stored in the shared {@link MCPSseServer}. + * + *

Both streaming and non-streaming modes are supported. + */ +public final class OpenAIProxyServlet extends HttpServlet { + + private static final Logger LOG = Logger.getLogger(OpenAIProxyServlet.class.getName()); + + private final MCPSseServer mcpServer; + + // HttpClient is built per-request from the session's OpenAIProxyConfig, + // which contains the proxy settings from the profile. + + public OpenAIProxyServlet(MCPSseServer mcpServer) { + this.mcpServer = mcpServer; + } + + @Override + protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws IOException { + // Extract UUID from path: /openai-proxy//v1/messages + String pathInfo = req.getPathInfo(); // e.g. "/abc-123/v1/messages" + if (pathInfo == null || pathInfo.length() < 2) { + resp.sendError(HttpServletResponse.SC_NOT_FOUND, "Missing session UUID in path"); + return; + } + String[] parts = pathInfo.substring(1).split("/", 2); // ["abc-123", "v1/messages"] + String uuid = parts[0]; + + OpenAIProxyConfig config = mcpServer.getOpenAIProxyConfig(uuid); + if (config == null) { + resp.sendError(HttpServletResponse.SC_NOT_FOUND, + "No active proxy session for UUID: " + uuid); + return; + } + + boolean debug = ClaudeCodePreferences.isDebugMode(); + + // Read Anthropic request body + String body = readBody(req); + JsonNode anthropicReq; + try { + anthropicReq = AnthropicToOpenAITranslator.MAPPER.readTree(body); + } catch (Exception e) { + LOG.severe("OpenAI proxy: failed to parse Anthropic request: " + e.getMessage()); + resp.sendError(HttpServletResponse.SC_BAD_REQUEST, "Invalid JSON"); + return; + } + + boolean streaming = anthropicReq.path("stream").asBoolean(false); + String model = anthropicReq.path("model").asText("unknown"); + + if (debug) { + StringBuilder sb = new StringBuilder("OpenAI proxy ← Anthropic request: "); + sb.append(AnthropicToOpenAITranslator.summarizeAnthropicRequest(anthropicReq)); + sb.append(" | headers:"); + Enumeration headerNames = req.getHeaderNames(); + while (headerNames.hasMoreElements()) { + String h = headerNames.nextElement(); + sb.append(" ").append(h).append("=").append(req.getHeader(h)); + } + LOG.info(sb.toString()); + } + + // Translate request + ObjectNode openaiReq = AnthropicToOpenAITranslator.translateRequest(anthropicReq); + String openaiBody = AnthropicToOpenAITranslator.MAPPER.writeValueAsString(openaiReq); + + String targetUrl = config.getBaseUrl(); + if (!targetUrl.endsWith("/")) targetUrl += "/"; + targetUrl += "v1/chat/completions"; + + if (debug) { + LOG.info("OpenAI proxy → OpenAI request: url=" + targetUrl + + " Authorization=Bearer [REDACTED]" + + " | " + AnthropicToOpenAITranslator.summarizeOpenAIRequest(openaiReq)); + } + + HttpRequest httpReq = HttpRequest.newBuilder() + .uri(URI.create(targetUrl)) + .header("Content-Type", "application/json") + .header("Authorization", "Bearer " + config.getApiKey()) + .POST(HttpRequest.BodyPublishers.ofString(openaiBody, StandardCharsets.UTF_8)) + .timeout(Duration.ofSeconds(600)) + .build(); + + HttpClient httpClient = config.buildHttpClient(); + if (streaming) { + handleStreaming(httpClient, httpReq, model, resp, debug); + } else { + handleNonStreaming(httpClient, httpReq, model, resp, debug); + } + } + + private void handleNonStreaming(HttpClient httpClient, HttpRequest httpReq, String model, + HttpServletResponse resp, boolean debug) throws IOException { + HttpResponse httpResp; + try { + httpResp = httpClient.send(httpReq, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8)); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + resp.sendError(HttpServletResponse.SC_BAD_GATEWAY, "Proxy request interrupted"); + return; + } + + int status = httpResp.statusCode(); + + if (status >= 400) { + String errBody = httpResp.body(); + LOG.severe("OpenAI proxy: provider returned HTTP " + status + ": " + errBody); + resp.setStatus(status); + resp.setContentType("application/json"); + resp.getWriter().write(toAnthropicError(errBody, status)); + return; + } + + JsonNode openaiResp; + try { + openaiResp = AnthropicToOpenAITranslator.MAPPER.readTree(httpResp.body()); + } catch (Exception e) { + LOG.severe("OpenAI proxy: failed to parse provider response: " + e.getMessage()); + resp.sendError(HttpServletResponse.SC_BAD_GATEWAY, "Invalid JSON from provider"); + return; + } + + if (debug) { + StringBuilder sb = new StringBuilder("OpenAI proxy ← OpenAI response: status=" + status); + httpResp.headers().map().forEach((k, v) -> + sb.append(" ").append(k).append("=").append(String.join(",", v))); + sb.append(" | ").append(AnthropicToOpenAITranslator.summarizeOpenAIResponse(openaiResp)); + LOG.info(sb.toString()); + } + + ObjectNode anthropicResp = AnthropicToOpenAITranslator.translateResponse(openaiResp, model); + + if (debug) { + LOG.info("OpenAI proxy → Anthropic response: stop_reason=" + + anthropicResp.path("stop_reason").asText() + + " input_tokens=" + anthropicResp.path("usage").path("input_tokens").asInt() + + " output_tokens=" + anthropicResp.path("usage").path("output_tokens").asInt()); + } + + resp.setStatus(HttpServletResponse.SC_OK); + resp.setContentType("application/json"); + resp.getWriter().write(AnthropicToOpenAITranslator.MAPPER.writeValueAsString(anthropicResp)); + } + + private void handleStreaming(HttpClient httpClient, HttpRequest httpReq, String model, + HttpServletResponse resp, boolean debug) throws IOException { + resp.setStatus(HttpServletResponse.SC_OK); + resp.setContentType("text/event-stream"); + resp.setCharacterEncoding("UTF-8"); + resp.setHeader("Cache-Control", "no-cache"); + resp.setHeader("Connection", "keep-alive"); + resp.setHeader("X-Accel-Buffering", "no"); + + PrintWriter writer = resp.getWriter(); + + HttpResponse httpResp; + try { + httpResp = httpClient.send(httpReq, HttpResponse.BodyHandlers.ofInputStream()); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + return; + } + + int status = httpResp.statusCode(); + if (status >= 400) { + String errBody = new String(httpResp.body().readAllBytes(), StandardCharsets.UTF_8); + LOG.severe("OpenAI proxy: provider returned HTTP " + status + " (streaming): " + errBody); + writer.write("event: error\ndata: " + toAnthropicError(errBody, status) + "\n\n"); + writer.flush(); + return; + } + + AnthropicToOpenAITranslator.StreamingState state = + new AnthropicToOpenAITranslator.StreamingState(); + + int chunkCount = 0; + boolean firstChunk = true; + + try (BufferedReader reader = new BufferedReader( + new InputStreamReader(httpResp.body(), StandardCharsets.UTF_8))) { + String line; + while ((line = reader.readLine()) != null) { + if (line.startsWith("data: ")) { + String data = line.substring(6).trim(); + if (firstChunk && debug) { + LOG.info("OpenAI proxy: first streaming chunk received"); + firstChunk = false; + } + String events = state.processChunk(data); + if (!events.isEmpty()) { + writer.write(events); + writer.flush(); + } + if (!"[DONE]".equals(data)) chunkCount++; + } + } + } + + if (debug) { + LOG.info("OpenAI proxy: streaming done, total chunks=" + chunkCount); + } + writer.flush(); + } + + /** + * Converts a provider error body to Anthropic error JSON string. + * Handles both Anthropic-format ({@code {"type":"error","error":{...}}}) + * and OpenAI-format ({@code {"error":{...}}}) provider responses. + */ + static String toAnthropicError(String providerBody, int httpStatus) { + try { + JsonNode json = AnthropicToOpenAITranslator.MAPPER.readTree(providerBody); + // Already Anthropic format + if (json.has("type") && json.has("error")) { + return providerBody; + } + // OpenAI format: {"error":{"message":"...","type":"..."}} + if (json.has("error")) { + JsonNode err = json.get("error"); + String msg = err.path("message").asText("Provider error " + httpStatus); + String type = err.path("type").asText("api_error"); + String escaped = msg.replace("\\", "\\\\").replace("\"", "\\\""); + return "{\"type\":\"error\",\"error\":{\"type\":\"" + type + "\",\"message\":\"" + escaped + "\"}}"; + } + } catch (Exception ignored) {} + return "{\"type\":\"error\",\"error\":{\"type\":\"api_error\",\"message\":\"Provider returned HTTP " + httpStatus + "\"}}"; + } + + private static String readBody(HttpServletRequest req) throws IOException { + StringBuilder sb = new StringBuilder(); + try (BufferedReader reader = req.getReader()) { + String line; + while ((line = reader.readLine()) != null) { + sb.append(line).append("\n"); + } + } + return sb.toString(); + } +} diff --git a/src/main/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfile.java b/src/main/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfile.java index cfa5965c..d4fb3743 100644 --- a/src/main/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfile.java +++ b/src/main/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfile.java @@ -51,7 +51,13 @@ public enum ConnectionType { CLAUDE_API, /** Third-party or custom API endpoint — injects {@code ANTHROPIC_AUTH_TOKEN} * and {@code ANTHROPIC_BASE_URL}. */ - OTHER_API + OTHER_API, + /** + * OpenAI-compatible provider routed through the internal proxy. + * The proxy is started by {@code ClaudeProcess} and injects its own + * {@code ANTHROPIC_BASE_URL}; no env vars are set here. + */ + OPENAI_PROXY } /** @@ -157,6 +163,13 @@ public enum ProxyMode { */ private String storageDir; + /** + * When {@code true}, this profile uses the internal OpenAI-compatible proxy. + * {@link #computeConnectionType()} returns {@link ConnectionType#OPENAI_PROXY}. + * {@link #baseUrl} and {@link #apiKey} hold the OpenAI endpoint URL and API key. + */ + private boolean openaiProxy; + // ------------------------------------------------------------------------- // No-arg constructor (Jackson) // ------------------------------------------------------------------------- @@ -235,6 +248,9 @@ public boolean isDefault() { * @return the connection type inferred from current field values */ public ConnectionType computeConnectionType() { + if (openaiProxy) { + return ConnectionType.OPENAI_PROXY; + } if (!isBlank(apiKey)) { return isBlank(baseUrl) ? ConnectionType.CLAUDE_API : ConnectionType.OTHER_API; } @@ -266,13 +282,14 @@ public Map toEnvVars() { // Auth switch (computeConnectionType()) { - case SUBSCRIPTION -> env.put("CLAUDE_CODE_OAUTH_TOKEN", blankToEmpty(token)); - case CLAUDE_API -> { /* API key is written to settings.local.json as apiKeyHelper */ } - case OTHER_API -> { + case SUBSCRIPTION -> env.put("CLAUDE_CODE_OAUTH_TOKEN", blankToEmpty(token)); + case CLAUDE_API -> { /* API key is written to settings.local.json as apiKeyHelper */ } + case OTHER_API -> { env.put("ANTHROPIC_AUTH_TOKEN", blankToEmpty(apiKey)); env.put("ANTHROPIC_BASE_URL", blankToEmpty(baseUrl)); } - default -> { /* CLAUDE_MANAGED — nothing to inject */ } + case OPENAI_PROXY -> { /* ANTHROPIC_BASE_URL is injected by ClaudeProcess after proxy start */ } + default -> { /* CLAUDE_MANAGED — nothing to inject */ } } // Proxy @@ -584,6 +601,12 @@ public ClaudeProfile withStorageDir(String storageDir) { return this; } + /** Returns {@code true} if this profile uses the internal OpenAI-compatible proxy. */ + public boolean isOpenaiProxy() { return openaiProxy; } + + /** Sets whether this profile uses the internal OpenAI-compatible proxy. */ + public void setOpenaiProxy(boolean openaiProxy) { this.openaiProxy = openaiProxy; } + // ------------------------------------------------------------------------- // Object // ------------------------------------------------------------------------- diff --git a/src/main/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfilesPanel.java b/src/main/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfilesPanel.java index 50059da8..16c8b1b0 100644 --- a/src/main/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfilesPanel.java +++ b/src/main/java/io/github/nbplugins/claudecodegui/settings/ClaudeProfilesPanel.java @@ -67,7 +67,8 @@ public final class ClaudeProfilesPanel extends JPanel { private static final String CONN_MANAGED = "Claude managed"; private static final String CONN_SUBSCRIPTION = "Subscription"; private static final String CONN_CLAUDE_API = "Claude API"; - private static final String CONN_OTHER_API = "Other API"; + private static final String CONN_OTHER_API = "Claude-compatible API"; + private static final String CONN_OPENAI_PROXY = "OpenAI-compatible API"; private static final String PROXY_CARD_NONE = "proxy_none"; private static final String PROXY_CARD_CUSTOM = "proxy_custom"; @@ -106,6 +107,9 @@ public final class ClaudeProfilesPanel extends JPanel { private JRadioButton rbClaudeApi; /** Radio button for other (custom base URL) API connection. */ private JRadioButton rbOtherApi; + /** Radio button for OpenAI-compatible proxy connection. */ + private JRadioButton rbOpenAIProxy; + /** Info label shown when OpenAI proxy is selected. */ /** Password field for the OAuth token. */ private JPasswordField tokenField; /** Password field for the Anthropic API key. */ @@ -290,8 +294,9 @@ private JPanel buildConnectionTypeSection() { rbSubscription = new JRadioButton(CONN_SUBSCRIPTION); rbClaudeApi = new JRadioButton(CONN_CLAUDE_API); rbOtherApi = new JRadioButton(CONN_OTHER_API); + rbOpenAIProxy = new JRadioButton(CONN_OPENAI_PROXY); ButtonGroup bg = new ButtonGroup(); - for (JRadioButton rb : new JRadioButton[]{rbManaged, rbSubscription, rbClaudeApi, rbOtherApi}) { + for (JRadioButton rb : new JRadioButton[]{rbManaged, rbSubscription, rbClaudeApi, rbOtherApi, rbOpenAIProxy}) { bg.add(rb); } @@ -339,6 +344,7 @@ private JPanel buildConnectionTypeSection() { rbSubscription.addActionListener(e -> updateFieldEnablement()); rbClaudeApi.addActionListener(e -> updateFieldEnablement()); rbOtherApi.addActionListener(e -> updateFieldEnablement()); + rbOpenAIProxy.addActionListener(e -> updateFieldEnablement()); // Grid: col0=radio, col1=label, col2=field(fill), col3=button JPanel grid = new JPanel(new GridBagLayout()); @@ -375,6 +381,9 @@ private JPanel buildConnectionTypeSection() { grid.add(baseUrlField, inlineFieldGbc(2, 3)); grid.add(modelAliasesBtn, inlineBtnGbc(3, 3)); + // row 4: rbOpenAIProxy — fields (baseUrlField, apiKeyField) are shared with rows 2-3 + grid.add(rbOpenAIProxy, inlineRbGbc(0, 4)); + JPanel outer = new JPanel(new BorderLayout(0, 4)); outer.add(new JLabel("Connection Type:"), BorderLayout.NORTH); outer.add(grid, BorderLayout.CENTER); @@ -382,14 +391,15 @@ private JPanel buildConnectionTypeSection() { } private void updateFieldEnablement() { - boolean sub = rbSubscription.isSelected(); - boolean api = rbClaudeApi.isSelected() || rbOtherApi.isSelected(); - boolean otherApi = rbOtherApi.isSelected(); + boolean sub = rbSubscription.isSelected(); + boolean api = rbClaudeApi.isSelected() || rbOtherApi.isSelected() || rbOpenAIProxy.isSelected(); + boolean otherApi = rbOtherApi.isSelected(); + boolean openai = rbOpenAIProxy.isSelected(); tokenField.setEnabled(sub); tokenShowBtn.setEnabled(sub); apiKeyField.setEnabled(api); apiKeyShowBtn.setEnabled(api); - baseUrlField.setEditable(otherApi); + baseUrlField.setEditable(otherApi || openai); if (claudeAiBtn != null) { claudeAiBtn.setVisible(sub); consoleAnthropicBtn.setVisible(rbClaudeApi.isSelected()); @@ -398,7 +408,7 @@ private void updateFieldEnablement() { } private void updateModelAliasesBtn() { - boolean other = rbOtherApi.isSelected(); + boolean other = rbOtherApi.isSelected() || rbOpenAIProxy.isSelected(); boolean hasKey = !new String(apiKeyField.getPassword()).isBlank(); boolean hasUrl = !baseUrlField.getText().isBlank(); modelAliasesBtn.setEnabled(other && hasKey && hasUrl); @@ -582,18 +592,27 @@ private void flushFormToCurrentProfile() { p.setToken(new String(tokenField.getPassword())); p.setApiKey(""); p.setBaseUrl(""); + p.setOpenaiProxy(false); } else if (rbClaudeApi.isSelected()) { p.setApiKey(new String(apiKeyField.getPassword())); p.setToken(""); p.setBaseUrl(""); + p.setOpenaiProxy(false); } else if (rbOtherApi.isSelected()) { p.setApiKey(new String(apiKeyField.getPassword())); p.setBaseUrl(baseUrlField.getText().trim()); p.setToken(""); + p.setOpenaiProxy(false); + } else if (rbOpenAIProxy.isSelected()) { + p.setApiKey(new String(apiKeyField.getPassword())); + p.setBaseUrl(baseUrlField.getText().trim()); + p.setToken(""); + p.setOpenaiProxy(true); } else { p.setToken(""); p.setApiKey(""); p.setBaseUrl(""); + p.setOpenaiProxy(false); } // Proxy @@ -658,6 +677,11 @@ private void loadProfileIntoForm(ClaudeProfile p) { apiKeyField.setText(p.getApiKey()); baseUrlField.setText(p.getBaseUrl()); } + case OPENAI_PROXY -> { + rbOpenAIProxy.setSelected(true); + apiKeyField.setText(p.getApiKey()); + baseUrlField.setText(p.getBaseUrl()); + } default -> rbManaged.setSelected(true); } updateFieldEnablement(); diff --git a/src/main/java/io/github/nbplugins/claudecodegui/settings/ProxyConfiguration.java b/src/main/java/io/github/nbplugins/claudecodegui/settings/ProxyConfiguration.java new file mode 100644 index 00000000..718d7289 --- /dev/null +++ b/src/main/java/io/github/nbplugins/claudecodegui/settings/ProxyConfiguration.java @@ -0,0 +1,77 @@ +package io.github.nbplugins.claudecodegui.settings; + +import io.github.nbplugins.claudecodegui.settings.ClaudeProfile.ProxyMode; +import java.net.InetSocketAddress; +import java.net.ProxySelector; +import java.net.URI; +import java.net.http.HttpClient; + +/** + * Immutable snapshot of HTTP proxy settings taken from a {@link ClaudeProfile}. + * + *

Used to configure {@link HttpClient} instances in components that make + * outbound HTTP requests on behalf of a session (e.g. the OpenAI-compatible proxy servlet). + */ +public final class ProxyConfiguration { + + private final ProxyMode mode; + private final String httpProxy; + private final String httpsProxy; + private final String noProxy; + + public ProxyConfiguration(ProxyMode mode, String httpProxy, String httpsProxy, String noProxy) { + this.mode = mode != null ? mode : ProxyMode.SYSTEM_MANAGED; + this.httpProxy = httpProxy != null ? httpProxy : ""; + this.httpsProxy = httpsProxy != null ? httpsProxy : ""; + this.noProxy = noProxy != null ? noProxy : ""; + } + + /** Creates a snapshot from a profile's current proxy settings. */ + public static ProxyConfiguration from(ClaudeProfile profile) { + return new ProxyConfiguration( + profile.getProxyMode(), + profile.getHttpProxy(), + profile.getHttpsProxy(), + profile.getNoProxy()); + } + + public ProxyMode getMode() { return mode; } + public String getHttpProxy() { return httpProxy; } + public String getHttpsProxy() { return httpsProxy; } + public String getNoProxy() { return noProxy; } + + /** + * Configures an {@link HttpClient.Builder} with this proxy configuration. + *

    + *
  • {@link ProxyMode#SYSTEM_MANAGED} — uses the JVM default proxy selector
  • + *
  • {@link ProxyMode#NO_PROXY} — disables proxying
  • + *
  • {@link ProxyMode#CUSTOM} — uses the HTTPS proxy URL, or HTTP proxy as fallback
  • + *
+ * + * @param builder the builder to configure (modified in place) + * @return the same builder, for chaining + */ + public HttpClient.Builder applyTo(HttpClient.Builder builder) { + switch (mode) { + case NO_PROXY -> + builder.proxy(HttpClient.Builder.NO_PROXY); + case CUSTOM -> { + String url = !httpsProxy.isBlank() ? httpsProxy + : !httpProxy.isBlank() ? httpProxy + : null; + if (url != null) { + try { + URI uri = URI.create(url); + int port = uri.getPort() > 0 ? uri.getPort() : 8080; + builder.proxy(ProxySelector.of(new InetSocketAddress(uri.getHost(), port))); + } catch (Exception e) { + builder.proxy(ProxySelector.getDefault()); + } + } + } + default -> // SYSTEM_MANAGED + builder.proxy(ProxySelector.getDefault()); + } + return builder; + } +} diff --git a/src/main/java/org/openbeans/claude/netbeans/ClaudeCodeStatusService.java b/src/main/java/org/openbeans/claude/netbeans/ClaudeCodeStatusService.java index edfc1b96..f8623b9b 100644 --- a/src/main/java/org/openbeans/claude/netbeans/ClaudeCodeStatusService.java +++ b/src/main/java/org/openbeans/claude/netbeans/ClaudeCodeStatusService.java @@ -1,5 +1,8 @@ package org.openbeans.claude.netbeans; +import io.github.nbplugins.claudecodegui.proxy.OpenAIProxyConfig; +import io.github.nbplugins.claudecodegui.settings.ProxyConfiguration; + /** * Service interface for accessing Claude Code integration status. * This service is registered in the global lookup to provide status information @@ -30,8 +33,35 @@ public interface ClaudeCodeStatusService { /** * Checks if the lock file is valid and accessible. - * + * * @return true if lock file is valid, false otherwise */ boolean isLockFileValid(); + + /** + * Registers an OpenAI-compatible proxy session. + * Called by {@code ClaudeProcess} at session start. + * + * @param uuid unique session identifier + * @param baseUrl OpenAI-compatible endpoint base URL + * @param apiKey API key for the provider + * @param proxy proxy settings from the profile + */ + void registerOpenAIProxy(String uuid, String baseUrl, String apiKey, ProxyConfiguration proxy); + + /** + * Deregisters an OpenAI-compatible proxy session. + * Called by {@code ClaudeProcess} at session stop. + * + * @param uuid unique session identifier previously passed to {@link #registerOpenAIProxy} + */ + void deregisterOpenAIProxy(String uuid); + + /** + * Returns the proxy config for the given session UUID, or {@code null} if not found. + * + * @param uuid unique session identifier + * @return config snapshot, or {@code null} + */ + OpenAIProxyConfig getOpenAIProxyConfig(String uuid); } \ No newline at end of file diff --git a/src/test/java/io/github/nbplugins/claudecodegui/proxy/AnthropicToOpenAITranslatorTest.java b/src/test/java/io/github/nbplugins/claudecodegui/proxy/AnthropicToOpenAITranslatorTest.java new file mode 100644 index 00000000..2c6dfece --- /dev/null +++ b/src/test/java/io/github/nbplugins/claudecodegui/proxy/AnthropicToOpenAITranslatorTest.java @@ -0,0 +1,277 @@ +package io.github.nbplugins.claudecodegui.proxy; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * Unit tests for {@link AnthropicToOpenAITranslator}. + * + *

JSON fixtures are in + * {@code src/test/resources/io/github/nbplugins/claudecodegui/proxy/}. + */ +class AnthropicToOpenAITranslatorTest { + + private static JsonNode load(String name) throws Exception { + String path = "/io/github/nbplugins/claudecodegui/proxy/" + name; + try (InputStream is = AnthropicToOpenAITranslatorTest.class.getResourceAsStream(path)) { + assertNotNull(is, "Missing fixture: " + path); + return AnthropicToOpenAITranslator.MAPPER.readTree( + new String(is.readAllBytes(), StandardCharsets.UTF_8)); + } + } + + private static AnthropicToOpenAITranslator.StreamingState state() { + return new AnthropicToOpenAITranslator.StreamingState(); + } + + // ------------------------------------------------------------------------- + // translateRequest + // ------------------------------------------------------------------------- + + @Test + void translateRequest_simpleTextMessage() throws Exception { + ObjectNode result = AnthropicToOpenAITranslator.translateRequest(load("req_simple_text.json")); + + assertEquals("claude-sonnet-4-5", result.path("model").asText()); + assertEquals(1024, result.path("max_tokens").asInt()); + JsonNode messages = result.path("messages"); + assertEquals(1, messages.size()); + assertEquals("user", messages.get(0).path("role").asText()); + assertEquals("Hello", messages.get(0).path("content").asText()); + } + + @Test + void translateRequest_systemPromptPrependedAsFirstMessage() throws Exception { + ObjectNode result = AnthropicToOpenAITranslator.translateRequest(load("req_system_prompt.json")); + + JsonNode messages = result.path("messages"); + assertEquals(2, messages.size()); + assertEquals("system", messages.get(0).path("role").asText()); + assertEquals("You are helpful.", messages.get(0).path("content").asText()); + assertEquals("user", messages.get(1).path("role").asText()); + } + + @Test + void translateRequest_systemAsListOfBlocks() throws Exception { + ObjectNode result = AnthropicToOpenAITranslator.translateRequest(load("req_system_as_blocks.json")); + + JsonNode sysMsg = result.path("messages").get(0); + assertEquals("system", sysMsg.path("role").asText()); + assertEquals("Block one.\n\nBlock two.", sysMsg.path("content").asText()); + } + + @Test + void translateRequest_toolUseInAssistantMessage() throws Exception { + ObjectNode result = AnthropicToOpenAITranslator.translateRequest(load("req_tool_use_assistant.json")); + + JsonNode msg = result.path("messages").get(0); + assertEquals("assistant", msg.path("role").asText()); + assertTrue(msg.path("content").isNull()); + JsonNode toolCalls = msg.path("tool_calls"); + assertEquals(1, toolCalls.size()); + assertEquals("call_1", toolCalls.get(0).path("id").asText()); + assertEquals("function", toolCalls.get(0).path("type").asText()); + assertEquals("read_file", toolCalls.get(0).path("function").path("name").asText()); + String args = toolCalls.get(0).path("function").path("arguments").asText(); + assertTrue(args.contains("test.txt"), "arguments should contain path: " + args); + } + + @Test + void translateRequest_toolResultInUserMessage() throws Exception { + ObjectNode result = AnthropicToOpenAITranslator.translateRequest(load("req_tool_result_user.json")); + + JsonNode messages = result.path("messages"); + assertEquals(1, messages.size()); + JsonNode toolMsg = messages.get(0); + assertEquals("tool", toolMsg.path("role").asText()); + assertEquals("call_1", toolMsg.path("tool_call_id").asText()); + assertEquals("file content here", toolMsg.path("content").asText()); + } + + @Test + void translateRequest_toolResultWithListContent() throws Exception { + ObjectNode result = AnthropicToOpenAITranslator.translateRequest(load("req_tool_result_list_content.json")); + + JsonNode toolMsg = result.path("messages").get(0); + assertEquals("tool", toolMsg.path("role").asText()); + assertEquals("result text", toolMsg.path("content").asText()); + } + + @Test + void translateRequest_toolsConverted() throws Exception { + ObjectNode result = AnthropicToOpenAITranslator.translateRequest(load("req_tools.json")); + + JsonNode tools = result.path("tools"); + assertEquals(1, tools.size()); + JsonNode tool = tools.get(0); + assertEquals("function", tool.path("type").asText()); + assertEquals("read_file", tool.path("function").path("name").asText()); + assertEquals("Reads a file", tool.path("function").path("description").asText()); + assertTrue(tool.path("function").path("parameters").has("properties")); + } + + @Test + void translateRequest_maxTokensCappedForNonClaudeModel() throws Exception { + ObjectNode result = AnthropicToOpenAITranslator.translateRequest(load("req_max_tokens_non_claude.json")); + assertEquals(16384, result.path("max_tokens").asInt()); + } + + @Test + void translateRequest_maxTokensNotCappedForClaudeModel() throws Exception { + ObjectNode result = AnthropicToOpenAITranslator.translateRequest(load("req_max_tokens_claude.json")); + assertEquals(100000, result.path("max_tokens").asInt()); + } + + @Test + void translateRequest_mixedAssistantContent() throws Exception { + ObjectNode result = AnthropicToOpenAITranslator.translateRequest(load("req_mixed_assistant.json")); + + JsonNode msg = result.path("messages").get(0); + assertEquals("I will call the tool.", msg.path("content").asText()); + assertEquals(1, msg.path("tool_calls").size()); + } + + // ------------------------------------------------------------------------- + // translateResponse + // ------------------------------------------------------------------------- + + @Test + void translateResponse_textContent() throws Exception { + ObjectNode result = AnthropicToOpenAITranslator.translateResponse(load("resp_text.json"), "gpt-4o"); + + assertEquals("message", result.path("type").asText()); + assertEquals("assistant", result.path("role").asText()); + assertEquals("end_turn", result.path("stop_reason").asText()); + assertEquals(1, result.path("content").size()); + assertEquals("text", result.path("content").get(0).path("type").asText()); + assertEquals("Hello there!", result.path("content").get(0).path("text").asText()); + assertEquals(10, result.path("usage").path("input_tokens").asInt()); + assertEquals(5, result.path("usage").path("output_tokens").asInt()); + } + + @Test + void translateResponse_toolCalls() throws Exception { + ObjectNode result = AnthropicToOpenAITranslator.translateResponse(load("resp_tool_calls.json"), "gpt-4o"); + + assertEquals("tool_use", result.path("stop_reason").asText()); + JsonNode content = result.path("content"); + assertEquals(1, content.size()); + assertEquals("tool_use", content.get(0).path("type").asText()); + assertEquals("call_abc", content.get(0).path("id").asText()); + assertEquals("read_file", content.get(0).path("name").asText()); + assertEquals("/tmp/x.txt", content.get(0).path("input").path("path").asText()); + } + + @Test + void translateResponse_finishReasonMapping() { + assertEquals("end_turn", AnthropicToOpenAITranslator.mapFinishReason("stop")); + assertEquals("max_tokens", AnthropicToOpenAITranslator.mapFinishReason("length")); + assertEquals("tool_use", AnthropicToOpenAITranslator.mapFinishReason("tool_calls")); + assertEquals("tool_use", AnthropicToOpenAITranslator.mapFinishReason("function_call")); + assertEquals("end_turn", AnthropicToOpenAITranslator.mapFinishReason("unknown")); + } + + @Test + void translateRequest_thinkingBlockConverted() throws Exception { + ObjectNode result = AnthropicToOpenAITranslator.translateRequest(load("req_thinking_assistant.json")); + + JsonNode msg = result.path("messages").get(0); + assertEquals("assistant", msg.path("role").asText()); + assertEquals("Let me reason about this.", msg.path("reasoning_content").asText()); + assertEquals("Here is the answer.", msg.path("content").asText()); + } + + // ------------------------------------------------------------------------- + // StreamingState + // ------------------------------------------------------------------------- + + @Test + void streaming_textDelta() { + var s = state(); + + String first = s.processChunk( + "{\"id\":\"c1\",\"model\":\"gpt-4o\",\"choices\":[{\"delta\":{\"role\":\"assistant\",\"content\":\"\"},\"index\":0}]}"); + assertTrue(first.contains("message_start"), "expected message_start in: " + first); + assertTrue(first.contains("ping"), "expected ping in: " + first); + + String second = s.processChunk( + "{\"id\":\"c1\",\"choices\":[{\"delta\":{\"content\":\"Hello\"},\"index\":0}]}"); + assertTrue(second.contains("content_block_start"), "expected content_block_start in: " + second); + assertTrue(second.contains("text_delta"), "expected text_delta in: " + second); + assertTrue(second.contains("Hello"), "expected Hello in: " + second); + + String done = s.processChunk("[DONE]"); + assertTrue(done.contains("content_block_stop"), "expected content_block_stop in: " + done); + assertTrue(done.contains("message_delta"), "expected message_delta in: " + done); + assertTrue(done.contains("message_stop"), "expected message_stop in: " + done); + } + + @Test + void streaming_toolCallDelta() { + var s = state(); + + s.processChunk("{\"id\":\"c1\",\"model\":\"gpt-4o\",\"choices\":[{\"delta\":{\"role\":\"assistant\"},\"index\":0}]}"); + + String chunk1 = s.processChunk( + "{\"id\":\"c1\",\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":0,\"id\":\"tc1\",\"function\":{\"name\":\"read_file\",\"arguments\":\"\"}}]},\"index\":0}]}"); + assertTrue(chunk1.contains("content_block_start"), "expected content_block_start in: " + chunk1); + assertTrue(chunk1.contains("tool_use"), "expected tool_use in: " + chunk1); + assertTrue(chunk1.contains("tc1"), "expected id in: " + chunk1); + + String chunk2 = s.processChunk( + "{\"id\":\"c1\",\"choices\":[{\"delta\":{\"tool_calls\":[{\"index\":0,\"function\":{\"arguments\":\"{\\\"path\\\":\\\"\"}}]},\"index\":0}]}"); + assertTrue(chunk2.contains("input_json_delta"), "expected input_json_delta in: " + chunk2); + + String done = s.processChunk("[DONE]"); + assertTrue(done.contains("content_block_stop"), "done should close tool block: " + done); + assertTrue(done.contains("message_stop"), "done should end message: " + done); + } + + @Test + void streaming_finishReasonInChunk() { + var s = state(); + s.processChunk("{\"id\":\"c1\",\"model\":\"m\",\"choices\":[{\"delta\":{\"content\":\"hi\"},\"index\":0}]}"); + s.processChunk("{\"id\":\"c1\",\"choices\":[{\"delta\":{},\"finish_reason\":\"length\",\"index\":0}]}"); + String done = s.processChunk("[DONE]"); + assertTrue(done.contains("max_tokens"), "expected max_tokens stop_reason in: " + done); + } + + @Test + void streaming_thinkingDelta() { + var s = state(); + + s.processChunk("{\"id\":\"c1\",\"model\":\"big-pickle\",\"choices\":[{\"delta\":{\"role\":\"assistant\"},\"index\":0}]}"); + + String chunk1 = s.processChunk( + "{\"id\":\"c1\",\"choices\":[{\"delta\":{\"reasoning_content\":\"Let me think.\"},\"index\":0}]}"); + assertTrue(chunk1.contains("content_block_start"), "expected thinking block start: " + chunk1); + assertTrue(chunk1.contains("thinking_delta"), "expected thinking_delta: " + chunk1); + assertTrue(chunk1.contains("Let me think."), "expected thinking text: " + chunk1); + + String chunk2 = s.processChunk( + "{\"id\":\"c1\",\"choices\":[{\"delta\":{\"content\":\"Answer.\"},\"index\":0}]}"); + assertTrue(chunk2.contains("signature_delta"), "expected signature_delta before text: " + chunk2); + assertTrue(chunk2.contains("content_block_stop"), "expected thinking block stop: " + chunk2); + assertTrue(chunk2.contains("text_delta"), "expected text_delta: " + chunk2); + + String done = s.processChunk("[DONE]"); + assertTrue(done.contains("message_stop"), "expected message_stop: " + done); + } + + @Test + void streaming_invalidJsonIsNoop() { + var s = state(); + assertEquals("", s.processChunk("not-valid-json")); + } + + @Test + void sseEvent_format() { + String event = AnthropicToOpenAITranslator.sseEvent("ping", "{\"type\":\"ping\"}"); + assertEquals("event: ping\ndata: {\"type\":\"ping\"}\n\n", event); + } +} diff --git a/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_max_tokens_claude.json b/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_max_tokens_claude.json new file mode 100644 index 00000000..509971b8 --- /dev/null +++ b/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_max_tokens_claude.json @@ -0,0 +1,5 @@ +{ + "model": "claude-opus-4-7", + "max_tokens": 100000, + "messages": [{"role": "user", "content": "Hi"}] +} diff --git a/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_max_tokens_non_claude.json b/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_max_tokens_non_claude.json new file mode 100644 index 00000000..d68232f5 --- /dev/null +++ b/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_max_tokens_non_claude.json @@ -0,0 +1,5 @@ +{ + "model": "gpt-4o", + "max_tokens": 100000, + "messages": [{"role": "user", "content": "Hi"}] +} diff --git a/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_mixed_assistant.json b/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_mixed_assistant.json new file mode 100644 index 00000000..8bf76030 --- /dev/null +++ b/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_mixed_assistant.json @@ -0,0 +1,11 @@ +{ + "model": "gpt-4o", + "max_tokens": 512, + "messages": [{ + "role": "assistant", + "content": [ + {"type": "text", "text": "I will call the tool."}, + {"type": "tool_use", "id": "t1", "name": "fn", "input": {}} + ] + }] +} diff --git a/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_simple_text.json b/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_simple_text.json new file mode 100644 index 00000000..27789993 --- /dev/null +++ b/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_simple_text.json @@ -0,0 +1,5 @@ +{ + "model": "claude-sonnet-4-5", + "max_tokens": 1024, + "messages": [{"role": "user", "content": [{"type": "text", "text": "Hello"}]}] +} diff --git a/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_system_as_blocks.json b/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_system_as_blocks.json new file mode 100644 index 00000000..40f9cdba --- /dev/null +++ b/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_system_as_blocks.json @@ -0,0 +1,9 @@ +{ + "model": "gpt-4o", + "max_tokens": 512, + "system": [ + {"type": "text", "text": "Block one."}, + {"type": "text", "text": "Block two."} + ], + "messages": [{"role": "user", "content": "Hi"}] +} diff --git a/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_system_prompt.json b/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_system_prompt.json new file mode 100644 index 00000000..57ab2ee2 --- /dev/null +++ b/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_system_prompt.json @@ -0,0 +1,6 @@ +{ + "model": "gpt-4o", + "max_tokens": 512, + "system": "You are helpful.", + "messages": [{"role": "user", "content": "Hi"}] +} diff --git a/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_thinking_assistant.json b/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_thinking_assistant.json new file mode 100644 index 00000000..932c4aae --- /dev/null +++ b/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_thinking_assistant.json @@ -0,0 +1,11 @@ +{ + "model": "big-pickle", + "max_tokens": 1024, + "messages": [{ + "role": "assistant", + "content": [ + {"type": "thinking", "thinking": "Let me reason about this.", "signature": "orig_sig"}, + {"type": "text", "text": "Here is the answer."} + ] + }] +} diff --git a/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_tool_result_list_content.json b/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_tool_result_list_content.json new file mode 100644 index 00000000..6d75f3f8 --- /dev/null +++ b/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_tool_result_list_content.json @@ -0,0 +1,12 @@ +{ + "model": "gpt-4o", + "max_tokens": 512, + "messages": [{ + "role": "user", + "content": [{ + "type": "tool_result", + "tool_use_id": "call_2", + "content": [{"type": "text", "text": "result text"}] + }] + }] +} diff --git a/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_tool_result_user.json b/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_tool_result_user.json new file mode 100644 index 00000000..b0c5ff64 --- /dev/null +++ b/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_tool_result_user.json @@ -0,0 +1,12 @@ +{ + "model": "gpt-4o", + "max_tokens": 512, + "messages": [{ + "role": "user", + "content": [{ + "type": "tool_result", + "tool_use_id": "call_1", + "content": "file content here" + }] + }] +} diff --git a/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_tool_use_assistant.json b/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_tool_use_assistant.json new file mode 100644 index 00000000..f0f139bb --- /dev/null +++ b/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_tool_use_assistant.json @@ -0,0 +1,13 @@ +{ + "model": "gpt-4o", + "max_tokens": 512, + "messages": [{ + "role": "assistant", + "content": [{ + "type": "tool_use", + "id": "call_1", + "name": "read_file", + "input": {"path": "/tmp/test.txt"} + }] + }] +} diff --git a/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_tools.json b/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_tools.json new file mode 100644 index 00000000..773f919b --- /dev/null +++ b/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_tools.json @@ -0,0 +1,13 @@ +{ + "model": "gpt-4o", + "max_tokens": 512, + "messages": [{"role": "user", "content": "use tool"}], + "tools": [{ + "name": "read_file", + "description": "Reads a file", + "input_schema": { + "type": "object", + "properties": {"path": {"type": "string"}} + } + }] +} diff --git a/src/test/resources/io/github/nbplugins/claudecodegui/proxy/resp_text.json b/src/test/resources/io/github/nbplugins/claudecodegui/proxy/resp_text.json new file mode 100644 index 00000000..a3095aa5 --- /dev/null +++ b/src/test/resources/io/github/nbplugins/claudecodegui/proxy/resp_text.json @@ -0,0 +1,8 @@ +{ + "id": "chatcmpl-123", + "choices": [{ + "message": {"role": "assistant", "content": "Hello there!"}, + "finish_reason": "stop" + }], + "usage": {"prompt_tokens": 10, "completion_tokens": 5} +} diff --git a/src/test/resources/io/github/nbplugins/claudecodegui/proxy/resp_tool_calls.json b/src/test/resources/io/github/nbplugins/claudecodegui/proxy/resp_tool_calls.json new file mode 100644 index 00000000..55b6cecb --- /dev/null +++ b/src/test/resources/io/github/nbplugins/claudecodegui/proxy/resp_tool_calls.json @@ -0,0 +1,19 @@ +{ + "id": "chatcmpl-456", + "choices": [{ + "message": { + "role": "assistant", + "content": null, + "tool_calls": [{ + "id": "call_abc", + "type": "function", + "function": { + "name": "read_file", + "arguments": "{\"path\":\"/tmp/x.txt\"}" + } + }] + }, + "finish_reason": "tool_calls" + }], + "usage": {"prompt_tokens": 20, "completion_tokens": 8} +} From 721f294ffa4a7363d32693c0393eaac70f920803 Mon Sep 17 00:00:00 2001 From: Oleg Samarin Date: Sat, 23 May 2026 10:04:43 +0300 Subject: [PATCH 2/3] Renamed package proxy to openaiproxy Co-Authored-By: Claude Sonnet 4.6 --- CLAUDE.md | 2 +- pom.xml | 2 +- .../github/nbplugins/claudecodegui/ClaudeCodeInstaller.java | 2 +- .../io/github/nbplugins/claudecodegui/mcp/MCPSseServer.java | 4 ++-- .../{proxy => openaiproxy}/AnthropicToOpenAITranslator.java | 2 +- .../{proxy => openaiproxy}/OpenAIProxyConfig.java | 2 +- .../{proxy => openaiproxy}/OpenAIProxyServlet.java | 2 +- .../openbeans/claude/netbeans/ClaudeCodeStatusService.java | 2 +- .../AnthropicToOpenAITranslatorTest.java | 2 +- .../{proxy => openaiproxy}/req_max_tokens_claude.json | 0 .../{proxy => openaiproxy}/req_max_tokens_non_claude.json | 0 .../{proxy => openaiproxy}/req_mixed_assistant.json | 0 .../claudecodegui/{proxy => openaiproxy}/req_simple_text.json | 0 .../{proxy => openaiproxy}/req_system_as_blocks.json | 0 .../{proxy => openaiproxy}/req_system_prompt.json | 0 .../{proxy => openaiproxy}/req_thinking_assistant.json | 0 .../{proxy => openaiproxy}/req_tool_result_list_content.json | 0 .../{proxy => openaiproxy}/req_tool_result_user.json | 0 .../{proxy => openaiproxy}/req_tool_use_assistant.json | 0 .../claudecodegui/{proxy => openaiproxy}/req_tools.json | 0 .../claudecodegui/{proxy => openaiproxy}/resp_text.json | 0 .../claudecodegui/{proxy => openaiproxy}/resp_tool_calls.json | 0 22 files changed, 10 insertions(+), 10 deletions(-) rename src/main/java/io/github/nbplugins/claudecodegui/{proxy => openaiproxy}/AnthropicToOpenAITranslator.java (99%) rename src/main/java/io/github/nbplugins/claudecodegui/{proxy => openaiproxy}/OpenAIProxyConfig.java (96%) rename src/main/java/io/github/nbplugins/claudecodegui/{proxy => openaiproxy}/OpenAIProxyServlet.java (99%) rename src/test/java/io/github/nbplugins/claudecodegui/{proxy => openaiproxy}/AnthropicToOpenAITranslatorTest.java (99%) rename src/test/resources/io/github/nbplugins/claudecodegui/{proxy => openaiproxy}/req_max_tokens_claude.json (100%) rename src/test/resources/io/github/nbplugins/claudecodegui/{proxy => openaiproxy}/req_max_tokens_non_claude.json (100%) rename src/test/resources/io/github/nbplugins/claudecodegui/{proxy => openaiproxy}/req_mixed_assistant.json (100%) rename src/test/resources/io/github/nbplugins/claudecodegui/{proxy => openaiproxy}/req_simple_text.json (100%) rename src/test/resources/io/github/nbplugins/claudecodegui/{proxy => openaiproxy}/req_system_as_blocks.json (100%) rename src/test/resources/io/github/nbplugins/claudecodegui/{proxy => openaiproxy}/req_system_prompt.json (100%) rename src/test/resources/io/github/nbplugins/claudecodegui/{proxy => openaiproxy}/req_thinking_assistant.json (100%) rename src/test/resources/io/github/nbplugins/claudecodegui/{proxy => openaiproxy}/req_tool_result_list_content.json (100%) rename src/test/resources/io/github/nbplugins/claudecodegui/{proxy => openaiproxy}/req_tool_result_user.json (100%) rename src/test/resources/io/github/nbplugins/claudecodegui/{proxy => openaiproxy}/req_tool_use_assistant.json (100%) rename src/test/resources/io/github/nbplugins/claudecodegui/{proxy => openaiproxy}/req_tools.json (100%) rename src/test/resources/io/github/nbplugins/claudecodegui/{proxy => openaiproxy}/resp_text.json (100%) rename src/test/resources/io/github/nbplugins/claudecodegui/{proxy => openaiproxy}/resp_tool_calls.json (100%) diff --git a/CLAUDE.md b/CLAUDE.md index d20986fd..bb67a538 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -177,7 +177,7 @@ The plugin follows an MVC structure for session management: | `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` `/openai-proxy/*`; `NetBeansMCPHandler` — MCP dispatcher + PreToolUse hook; `tools/` — `OpenDiff`, `PermissionPromptTool`, `DiffTabTracker`, `GetDiagnostics`, `GetOpenEditors`, `GetCurrentSelection`, `OpenFile` | -| `io.github.nbplugins.claudecodegui.proxy` | `OpenAIProxyConfig` — per-session config snapshot; `OpenAIProxyServlet` — translates Anthropic → OpenAI requests and responses; `AnthropicToOpenAITranslator` — stateless translation logic including streaming SSE state machine | +| `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 diff --git a/pom.xml b/pom.xml index 9cf54b96..ea4b004c 100644 --- a/pom.xml +++ b/pom.xml @@ -6,7 +6,7 @@ io.github.nbplugins netbeans-plugin-claude-code-gui - 1.2.5-SNAPSHOT + 1.2.7-SNAPSHOT nbm Netbeans Plugin Claude Code GUI NetBeans plugin that provides a GUI for Claude Code CLI — run claude code sessions directly inside the IDE. diff --git a/src/main/java/io/github/nbplugins/claudecodegui/ClaudeCodeInstaller.java b/src/main/java/io/github/nbplugins/claudecodegui/ClaudeCodeInstaller.java index 4ca2650f..3246cff3 100644 --- a/src/main/java/io/github/nbplugins/claudecodegui/ClaudeCodeInstaller.java +++ b/src/main/java/io/github/nbplugins/claudecodegui/ClaudeCodeInstaller.java @@ -264,7 +264,7 @@ public void deregisterOpenAIProxy(String uuid) { } @Override - public io.github.nbplugins.claudecodegui.proxy.OpenAIProxyConfig getOpenAIProxyConfig(String uuid) { + public io.github.nbplugins.claudecodegui.openaiproxy.OpenAIProxyConfig getOpenAIProxyConfig(String uuid) { return mcpServer != null ? mcpServer.getOpenAIProxyConfig(uuid) : null; } } diff --git a/src/main/java/io/github/nbplugins/claudecodegui/mcp/MCPSseServer.java b/src/main/java/io/github/nbplugins/claudecodegui/mcp/MCPSseServer.java index 725fb036..8dc675f0 100644 --- a/src/main/java/io/github/nbplugins/claudecodegui/mcp/MCPSseServer.java +++ b/src/main/java/io/github/nbplugins/claudecodegui/mcp/MCPSseServer.java @@ -4,8 +4,8 @@ import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; -import io.github.nbplugins.claudecodegui.proxy.OpenAIProxyConfig; -import io.github.nbplugins.claudecodegui.proxy.OpenAIProxyServlet; +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; diff --git a/src/main/java/io/github/nbplugins/claudecodegui/proxy/AnthropicToOpenAITranslator.java b/src/main/java/io/github/nbplugins/claudecodegui/openaiproxy/AnthropicToOpenAITranslator.java similarity index 99% rename from src/main/java/io/github/nbplugins/claudecodegui/proxy/AnthropicToOpenAITranslator.java rename to src/main/java/io/github/nbplugins/claudecodegui/openaiproxy/AnthropicToOpenAITranslator.java index 27a369c2..00d743e0 100644 --- a/src/main/java/io/github/nbplugins/claudecodegui/proxy/AnthropicToOpenAITranslator.java +++ b/src/main/java/io/github/nbplugins/claudecodegui/openaiproxy/AnthropicToOpenAITranslator.java @@ -1,4 +1,4 @@ -package io.github.nbplugins.claudecodegui.proxy; +package io.github.nbplugins.claudecodegui.openaiproxy; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; diff --git a/src/main/java/io/github/nbplugins/claudecodegui/proxy/OpenAIProxyConfig.java b/src/main/java/io/github/nbplugins/claudecodegui/openaiproxy/OpenAIProxyConfig.java similarity index 96% rename from src/main/java/io/github/nbplugins/claudecodegui/proxy/OpenAIProxyConfig.java rename to src/main/java/io/github/nbplugins/claudecodegui/openaiproxy/OpenAIProxyConfig.java index c659a7f1..8b65bb60 100644 --- a/src/main/java/io/github/nbplugins/claudecodegui/proxy/OpenAIProxyConfig.java +++ b/src/main/java/io/github/nbplugins/claudecodegui/openaiproxy/OpenAIProxyConfig.java @@ -1,4 +1,4 @@ -package io.github.nbplugins.claudecodegui.proxy; +package io.github.nbplugins.claudecodegui.openaiproxy; import io.github.nbplugins.claudecodegui.settings.ProxyConfiguration; import java.net.http.HttpClient; diff --git a/src/main/java/io/github/nbplugins/claudecodegui/proxy/OpenAIProxyServlet.java b/src/main/java/io/github/nbplugins/claudecodegui/openaiproxy/OpenAIProxyServlet.java similarity index 99% rename from src/main/java/io/github/nbplugins/claudecodegui/proxy/OpenAIProxyServlet.java rename to src/main/java/io/github/nbplugins/claudecodegui/openaiproxy/OpenAIProxyServlet.java index 2a37fffd..0e528e96 100644 --- a/src/main/java/io/github/nbplugins/claudecodegui/proxy/OpenAIProxyServlet.java +++ b/src/main/java/io/github/nbplugins/claudecodegui/openaiproxy/OpenAIProxyServlet.java @@ -1,4 +1,4 @@ -package io.github.nbplugins.claudecodegui.proxy; +package io.github.nbplugins.claudecodegui.openaiproxy; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; diff --git a/src/main/java/org/openbeans/claude/netbeans/ClaudeCodeStatusService.java b/src/main/java/org/openbeans/claude/netbeans/ClaudeCodeStatusService.java index f8623b9b..72bc1577 100644 --- a/src/main/java/org/openbeans/claude/netbeans/ClaudeCodeStatusService.java +++ b/src/main/java/org/openbeans/claude/netbeans/ClaudeCodeStatusService.java @@ -1,6 +1,6 @@ package org.openbeans.claude.netbeans; -import io.github.nbplugins.claudecodegui.proxy.OpenAIProxyConfig; +import io.github.nbplugins.claudecodegui.openaiproxy.OpenAIProxyConfig; import io.github.nbplugins.claudecodegui.settings.ProxyConfiguration; /** diff --git a/src/test/java/io/github/nbplugins/claudecodegui/proxy/AnthropicToOpenAITranslatorTest.java b/src/test/java/io/github/nbplugins/claudecodegui/openaiproxy/AnthropicToOpenAITranslatorTest.java similarity index 99% rename from src/test/java/io/github/nbplugins/claudecodegui/proxy/AnthropicToOpenAITranslatorTest.java rename to src/test/java/io/github/nbplugins/claudecodegui/openaiproxy/AnthropicToOpenAITranslatorTest.java index 2c6dfece..22af9d76 100644 --- a/src/test/java/io/github/nbplugins/claudecodegui/proxy/AnthropicToOpenAITranslatorTest.java +++ b/src/test/java/io/github/nbplugins/claudecodegui/openaiproxy/AnthropicToOpenAITranslatorTest.java @@ -1,4 +1,4 @@ -package io.github.nbplugins.claudecodegui.proxy; +package io.github.nbplugins.claudecodegui.openaiproxy; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.node.ObjectNode; diff --git a/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_max_tokens_claude.json b/src/test/resources/io/github/nbplugins/claudecodegui/openaiproxy/req_max_tokens_claude.json similarity index 100% rename from src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_max_tokens_claude.json rename to src/test/resources/io/github/nbplugins/claudecodegui/openaiproxy/req_max_tokens_claude.json diff --git a/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_max_tokens_non_claude.json b/src/test/resources/io/github/nbplugins/claudecodegui/openaiproxy/req_max_tokens_non_claude.json similarity index 100% rename from src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_max_tokens_non_claude.json rename to src/test/resources/io/github/nbplugins/claudecodegui/openaiproxy/req_max_tokens_non_claude.json diff --git a/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_mixed_assistant.json b/src/test/resources/io/github/nbplugins/claudecodegui/openaiproxy/req_mixed_assistant.json similarity index 100% rename from src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_mixed_assistant.json rename to src/test/resources/io/github/nbplugins/claudecodegui/openaiproxy/req_mixed_assistant.json diff --git a/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_simple_text.json b/src/test/resources/io/github/nbplugins/claudecodegui/openaiproxy/req_simple_text.json similarity index 100% rename from src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_simple_text.json rename to src/test/resources/io/github/nbplugins/claudecodegui/openaiproxy/req_simple_text.json diff --git a/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_system_as_blocks.json b/src/test/resources/io/github/nbplugins/claudecodegui/openaiproxy/req_system_as_blocks.json similarity index 100% rename from src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_system_as_blocks.json rename to src/test/resources/io/github/nbplugins/claudecodegui/openaiproxy/req_system_as_blocks.json diff --git a/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_system_prompt.json b/src/test/resources/io/github/nbplugins/claudecodegui/openaiproxy/req_system_prompt.json similarity index 100% rename from src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_system_prompt.json rename to src/test/resources/io/github/nbplugins/claudecodegui/openaiproxy/req_system_prompt.json diff --git a/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_thinking_assistant.json b/src/test/resources/io/github/nbplugins/claudecodegui/openaiproxy/req_thinking_assistant.json similarity index 100% rename from src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_thinking_assistant.json rename to src/test/resources/io/github/nbplugins/claudecodegui/openaiproxy/req_thinking_assistant.json diff --git a/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_tool_result_list_content.json b/src/test/resources/io/github/nbplugins/claudecodegui/openaiproxy/req_tool_result_list_content.json similarity index 100% rename from src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_tool_result_list_content.json rename to src/test/resources/io/github/nbplugins/claudecodegui/openaiproxy/req_tool_result_list_content.json diff --git a/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_tool_result_user.json b/src/test/resources/io/github/nbplugins/claudecodegui/openaiproxy/req_tool_result_user.json similarity index 100% rename from src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_tool_result_user.json rename to src/test/resources/io/github/nbplugins/claudecodegui/openaiproxy/req_tool_result_user.json diff --git a/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_tool_use_assistant.json b/src/test/resources/io/github/nbplugins/claudecodegui/openaiproxy/req_tool_use_assistant.json similarity index 100% rename from src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_tool_use_assistant.json rename to src/test/resources/io/github/nbplugins/claudecodegui/openaiproxy/req_tool_use_assistant.json diff --git a/src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_tools.json b/src/test/resources/io/github/nbplugins/claudecodegui/openaiproxy/req_tools.json similarity index 100% rename from src/test/resources/io/github/nbplugins/claudecodegui/proxy/req_tools.json rename to src/test/resources/io/github/nbplugins/claudecodegui/openaiproxy/req_tools.json diff --git a/src/test/resources/io/github/nbplugins/claudecodegui/proxy/resp_text.json b/src/test/resources/io/github/nbplugins/claudecodegui/openaiproxy/resp_text.json similarity index 100% rename from src/test/resources/io/github/nbplugins/claudecodegui/proxy/resp_text.json rename to src/test/resources/io/github/nbplugins/claudecodegui/openaiproxy/resp_text.json diff --git a/src/test/resources/io/github/nbplugins/claudecodegui/proxy/resp_tool_calls.json b/src/test/resources/io/github/nbplugins/claudecodegui/openaiproxy/resp_tool_calls.json similarity index 100% rename from src/test/resources/io/github/nbplugins/claudecodegui/proxy/resp_tool_calls.json rename to src/test/resources/io/github/nbplugins/claudecodegui/openaiproxy/resp_tool_calls.json From c8482b96028ec442c0812c6ad8dcf43bf7550c0b Mon Sep 17 00:00:00 2001 From: Oleg Samarin Date: Sat, 23 May 2026 10:16:32 +0300 Subject: [PATCH 3/3] =?UTF-8?q?Fixed=20hardcoded=20resource=20path=20in=20?= =?UTF-8?q?AnthropicToOpenAITranslatorTest=20after=20proxy=E2=86=92openaip?= =?UTF-8?q?roxy=20rename?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Sonnet 4.6 --- .../openaiproxy/AnthropicToOpenAITranslatorTest.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/test/java/io/github/nbplugins/claudecodegui/openaiproxy/AnthropicToOpenAITranslatorTest.java b/src/test/java/io/github/nbplugins/claudecodegui/openaiproxy/AnthropicToOpenAITranslatorTest.java index 22af9d76..9c89a4d8 100644 --- a/src/test/java/io/github/nbplugins/claudecodegui/openaiproxy/AnthropicToOpenAITranslatorTest.java +++ b/src/test/java/io/github/nbplugins/claudecodegui/openaiproxy/AnthropicToOpenAITranslatorTest.java @@ -12,12 +12,12 @@ * Unit tests for {@link AnthropicToOpenAITranslator}. * *

JSON fixtures are in - * {@code src/test/resources/io/github/nbplugins/claudecodegui/proxy/}. + * {@code src/test/resources/io/github/nbplugins/claudecodegui/openaiproxy/}. */ class AnthropicToOpenAITranslatorTest { private static JsonNode load(String name) throws Exception { - String path = "/io/github/nbplugins/claudecodegui/proxy/" + name; + String path = "/io/github/nbplugins/claudecodegui/openaiproxy/" + name; try (InputStream is = AnthropicToOpenAITranslatorTest.class.getResourceAsStream(path)) { assertNotNull(is, "Missing fixture: " + path); return AnthropicToOpenAITranslator.MAPPER.readTree(