diff --git a/README.md b/README.md index 9e01e2f..8ec9535 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,8 @@ A standalone plugin and native provider bridge for [OpenClaude](https://github.c Features: - **Authentic OpenCode v2 Harness Reverse Engineering**: Generates authentic 30-character descending session identifiers (`ses_`), client affinity tracking, and full harness request headers (`User-Agent`, `x-opencode-client`, `x-opencode-session`, `x-opencode-project`, `x-session-affinity`). - **Seamless Protocol Translation**: OpenClaude talks standard OpenAI `/v1/chat/completions`. Upstream models like **`muse-spark-1.3-contributor`** require OpenAI `/responses` API. The bridge transparently translates requests and bi-directionally streams Server-Sent Events (SSE). +- **1M Native Context Window**: Automatically configures OpenClaude for the full 1,048,576 token context window on `muse-spark-1.3-contributor` instead of falling back to 128K. +- **Configurable Reasoning Effort**: Unlocks OpenClaude's native `/effort` selector (`low`, `medium`, `high`, `xhigh`) and translates effort parameters into upstream OpenCode Go reasoning structures. - **Target Default Model**: Defaults to **`muse-spark-1.3-contributor`** with verified support for **`space-bunny`** and all 36+ OpenCode Go models. - **Model Name Normalizer**: Strips any client prefixes (`opencode-unlocked-*`, `opencode-go-*`) to prevent upstream rejection. - **Zero-Secret Distribution**: The repository contains no hardcoded keys or `.env` files. Users provide their key via environment variable, interactive command, or local config. @@ -88,12 +90,38 @@ Manage the background bridge daemon using `./bin/opencode-unlocked`: # Switch active default model ./bin/opencode-unlocked switch space-bunny +# View or set reasoning effort (low, medium, high, xhigh) +./bin/opencode-unlocked effort high + # Run live connectivity verification ./bin/opencode-unlocked test ``` --- +## 1M Context Window & Reasoning Effort + +### 1,048,576 Token Context Window +OpenCode Go's `muse-spark-1.3-contributor` natively supports a **1M (1,048,576 tokens)** context window. Normally, OpenClaude falls back to a 128,000 token limit on unknown OpenAI-compatible endpoints. + +Opencode Unlocked automatically configures: +- OpenClaude provider limits (`maxContextLength: 1048576`) +- `CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS` environment mapping +- OpenClaude `modelLimits` in `settings.json` + +### Multi-Level Reasoning Effort +OpenCode Go `/responses` models support four levels of reasoning: `low`, `medium`, `high`, and `xhigh`. + +You can adjust reasoning effort in two ways: +1. **In OpenClaude Interactively**: + Use the native `/effort` slash command or the interactive picker to select `low`, `medium`, `high`, or `xhigh`. +2. **Via Plugin CLI**: + ```bash + ./bin/opencode-unlocked effort medium + ``` + +--- + ## Supported Models All 36+ OpenCode Go models are available, including: diff --git a/bin/opencode-unlocked b/bin/opencode-unlocked index e3116e9..c0a9236 100755 --- a/bin/opencode-unlocked +++ b/bin/opencode-unlocked @@ -109,6 +109,8 @@ async function printStatus() { console.log(`Gateway Status : ${running ? "🟢 ONLINE" : "šŸ”“ OFFLINE"}`); console.log(`Gateway URL : http://${CONFIG.HOST}:${CONFIG.PORT}/v1`); console.log(`Default Model : ${config.defaultModel || CONFIG.DEFAULT_MODEL}`); + console.log(`Context Window : 1,048,576 tokens (1M native)`); + console.log(`Reasoning Effort: ${config.reasoningEffort || "auto (low, medium, high, xhigh supported)"}`); console.log(`API Key Loaded : ${apiKey ? `āœ” Configured (${apiKey.slice(0, 8)}...)` : "āŒ Missing"}`); console.log(`Config Path : ${getUserConfigPath()}`); @@ -156,6 +158,23 @@ async function setModel(modelName) { console.log(`āœ” Default model updated to: ${modelName}`); } +async function setEffort(effortLevel) { + if (!effortLevel) { + const config = loadUserConfig(); + console.log(`Current reasoning effort: ${config.reasoningEffort || "auto (model default)"}`); + console.log("Supported levels: low, medium, high, xhigh, auto"); + return; + } + const valid = ["low", "medium", "high", "xhigh", "auto"]; + const normalized = effortLevel.toLowerCase(); + if (!valid.includes(normalized)) { + console.error(`Invalid effort level "${effortLevel}". Valid options: ${valid.join(", ")}`); + process.exit(1); + } + saveUserConfig({ reasoningEffort: normalized }); + console.log(`āœ” Default reasoning effort updated to: ${normalized}`); +} + async function runTests() { console.log("Running live verification suite..."); const child = spawn(process.execPath, [path.join(ROOT_DIR, "test", "live-models.test.mjs")], { @@ -192,6 +211,10 @@ switch (command) { case "set-model": await setModel(args[1]); break; + case "effort": + case "reasoning": + await setEffort(args[1]); + break; case "test": await runTests(); break; @@ -210,12 +233,13 @@ Commands: start Start the local bridge daemon stop Stop the running daemon restart Restart the daemon - status Display daemon health, active model, and token usage + status Display daemon health, active model, context window, and token usage models List all 36+ available OpenCode Go models switch Change default model (e.g. space-bunny, muse-spark-1.3-contributor) + effort [level] View or set reasoning effort (low, medium, high, xhigh, auto) set-key Save OpenCode API key securely to user configuration test Run live verification on muse-spark and space-bunny - setup Configure OpenClaude provider profile to use this gateway + setup Configure OpenClaude provider profile to use this gateway (1M context & effort enabled) help Show this help dialog `); break; diff --git a/bin/setup.mjs b/bin/setup.mjs index 7bd1e02..11b1861 100755 --- a/bin/setup.mjs +++ b/bin/setup.mjs @@ -41,6 +41,7 @@ export function configureOpenClaudeProfile(options = {}) { provider: "openai", baseUrl, model: targetModel, + maxContextLength: 1048576, apiKey: "opencode-unlocked" }; @@ -55,19 +56,51 @@ export function configureOpenClaudeProfile(options = {}) { // Register model options cache for picker UI globalConfig.openaiAdditionalModelOptionsCacheByProfile = globalConfig.openaiAdditionalModelOptionsCacheByProfile || {}; globalConfig.openaiAdditionalModelOptionsCacheByProfile[PROVIDER_ID] = [ - { value: "muse-spark-1.3-contributor", label: "Muse Spark 1.3 Contributor", description: "Default Reasoning Model" }, + { value: "muse-spark-1.3-contributor", label: "Muse Spark 1.3 Contributor", description: "Default Reasoning Model (1M Context, Multi-Level Effort)" }, { value: "space-bunny", label: "Space Bunny", description: "Lightweight Chat & Code Model" }, { value: "deepseek-v4-flash", label: "DeepSeek V4 Flash", description: "High-Throughput Model" }, { value: "glm-5.3", label: "GLM 5.3", description: "General Language Model" } ]; fs.writeFileSync(globalConfigPath, JSON.stringify(globalConfig, null, 2), "utf-8"); - console.log(`āœ” Updated ~/.openclaude.json (active provider set to: ${PROVIDER_NAME})`); + console.log(`āœ” Updated ~/.openclaude.json (active provider set to: ${PROVIDER_NAME}, maxContextLength: 1M)`); } catch (err) { console.warn(`⚠ Could not update ~/.openclaude.json: ${err.message}`); } } + // 2. Configure modelLimits and reasoning effort in ~/.openclaude/settings.json + const settingsPath = path.join(openclaudeDir, "settings.json"); + if (fs.existsSync(settingsPath)) { + try { + const settings = JSON.parse(fs.readFileSync(settingsPath, "utf-8")); + settings.env = settings.env || {}; + settings.env.CLAUDE_CODE_ALWAYS_ENABLE_EFFORT = "1"; + settings.env.CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS = JSON.stringify({ + "muse-spark-1.3-contributor": 1048576, + "muse-spark-1.2-contributor": 1048576, + "muse-spark": 1048576 + }); + settings.modelLimits = settings.modelLimits || {}; + settings.modelLimits["muse-spark-1.3-contributor"] = { + contextWindow: 1048576, + maxOutputTokens: 65536 + }; + settings.modelLimits["muse-spark-1.2-contributor"] = { + contextWindow: 1048576, + maxOutputTokens: 65536 + }; + settings.modelLimits["muse-spark"] = { + contextWindow: 1048576, + maxOutputTokens: 65536 + }; + fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2), "utf-8"); + console.log(`āœ” Configured 1M context limits & reasoning effort in ~/.openclaude/settings.json`); + } catch (err) { + console.warn(`⚠ Could not update ~/.openclaude/settings.json: ${err.message}`); + } + } + // 2. Configure legacy mirror ~/.openclaude/.openclaude-profile.json if (!fs.existsSync(openclaudeDir)) { fs.mkdirSync(openclaudeDir, { recursive: true }); @@ -80,10 +113,17 @@ export function configureOpenClaudeProfile(options = {}) { const legacyProfile = { profile: "openai", + maxContextLength: 1048576, env: { OPENAI_BASE_URL: baseUrl, OPENAI_MODEL: targetModel, - OPENAI_API_KEY: "opencode-unlocked" + OPENAI_API_KEY: "opencode-unlocked", + CLAUDE_CODE_ALWAYS_ENABLE_EFFORT: "1", + CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS: JSON.stringify({ + "muse-spark-1.3-contributor": 1048576, + "muse-spark-1.2-contributor": 1048576, + "muse-spark": 1048576 + }) }, displayName: PROVIDER_NAME, createdAt: new Date().toISOString() diff --git a/commands/unlocked.md b/commands/unlocked.md index bd8c0e8..7290a37 100644 --- a/commands/unlocked.md +++ b/commands/unlocked.md @@ -1,11 +1,12 @@ --- -description: Manage Opencode Unlocked bridge, switch models, or configure keys -argument-hint: "[models|switch |key |test]" +description: Manage Opencode Unlocked bridge, switch models, configure keys, or set effort +argument-hint: "[models|switch |effort |key |test]" --- Manage Opencode Unlocked: - If argument is `models`: Run `${CLAUDE_PLUGIN_ROOT}/bin/opencode-unlocked models` to list all 36+ available models. - If argument is `switch `: Run `${CLAUDE_PLUGIN_ROOT}/bin/opencode-unlocked switch $1` to change the default model. +- If argument is `effort `: Run `${CLAUDE_PLUGIN_ROOT}/bin/opencode-unlocked effort $1` to set reasoning effort (low, medium, high, xhigh, auto). - If argument is `key `: Run `${CLAUDE_PLUGIN_ROOT}/bin/opencode-unlocked set-key $1` to securely store your OpenCode API key. - If argument is `test`: Run `${CLAUDE_PLUGIN_ROOT}/bin/opencode-unlocked test` to test live connectivity to Muse Spark 1.3 Contributor and Space Bunny. - Otherwise, run `${CLAUDE_PLUGIN_ROOT}/bin/opencode-unlocked status`. diff --git a/hooks/session-start.sh b/hooks/session-start.sh index 96967bd..c5b0428 100755 --- a/hooks/session-start.sh +++ b/hooks/session-start.sh @@ -42,6 +42,8 @@ export OPENAI_BASE_URL="${GATEWAY_URL}/v1" if [ -z "\$OPENAI_MODEL" ]; then export OPENAI_MODEL="muse-spark-1.3-contributor" fi +export CLAUDE_CODE_ALWAYS_ENABLE_EFFORT="1" +export CLAUDE_CODE_OPENAI_CONTEXT_WINDOWS='{"muse-spark-1.3-contributor":1048576,"muse-spark-1.2-contributor":1048576,"muse-spark":1048576}' EOF fi @@ -50,7 +52,7 @@ cat << EOF { "hookSpecificOutput": { "hookEventName": "SessionStart", - "additionalContext": "Opencode Unlocked active at ${GATEWAY_URL}/v1. Default model: muse-spark-1.3-contributor (Space Bunny and 36+ models available)." + "additionalContext": "Opencode Unlocked active at ${GATEWAY_URL}/v1. Default model: muse-spark-1.3-contributor (1M context window, multi-level reasoning effort)." } } EOF diff --git a/server/translator.mjs b/server/translator.mjs index a0b5298..f86d1d2 100644 --- a/server/translator.mjs +++ b/server/translator.mjs @@ -123,6 +123,14 @@ export function chatCompletionsToResponsesBody(body) { if (typeof max_output_tokens === "number") responsesPayload.max_output_tokens = max_output_tokens; else if (typeof max_tokens === "number") responsesPayload.max_output_tokens = max_tokens; + // Translate reasoning_effort (OpenAI Chat Completions) to reasoning: { effort } (Responses API) + const incomingEffort = body.reasoning_effort || (typeof body.reasoning === "object" ? body.reasoning?.effort : undefined); + if (incomingEffort && incomingEffort !== "auto" && incomingEffort !== "none") { + responsesPayload.reasoning = { + effort: incomingEffort + }; + } + if (Array.isArray(tools)) { responsesPayload.tools = tools.map((t) => { if (t.type === "function" && t.function) { diff --git a/skills/opencode-unlocked/SKILL.md b/skills/opencode-unlocked/SKILL.md index 1ee824e..16b1263 100644 --- a/skills/opencode-unlocked/SKILL.md +++ b/skills/opencode-unlocked/SKILL.md @@ -14,7 +14,7 @@ OpenCode Go hosts 36+ cutting-edge models behind strict harness fingerprinting. 3. **Model Prefix Stripping**: Normalizes prefix variations (`opencode-unlocked-*` -> `*`). ## Primary Models -- **`muse-spark-1.3-contributor`** (Default): High-capability reasoning and coding model. Requires `/responses` protocol; translated automatically by the bridge. +- **`muse-spark-1.3-contributor`** (Default): High-capability reasoning and coding model with a full **1M (1,048,576 token)** context window. Supports configurable reasoning effort (`low`, `medium`, `high`, `xhigh`). Requires `/responses` protocol; translated automatically by the bridge. - **`space-bunny`**: Fast, lightweight general and coding model. Native `/chat/completions`. - **`deepseek-v4-flash`**: High-throughput flash model. - **`glm-5.3`**: General language model. @@ -25,5 +25,6 @@ OpenCode Go hosts 36+ cutting-edge models behind strict harness fingerprinting. ## Operational Commands - Check daemon health and usage: `/opencode-unlocked:status` -- Manage models and keys: `/opencode-unlocked:unlocked` -- CLI Management: `opencode-unlocked [start|stop|restart|status|models|test|set-key]` +- Manage models, keys, and effort: `/opencode-unlocked:unlocked [models|switch |effort |key |test]` +- Adjust reasoning effort directly: `/effort [low|medium|high|xhigh|auto]` +- CLI Management: `opencode-unlocked [start|stop|restart|status|models|switch|effort|test|set-key|setup]` diff --git a/test/live-models.test.mjs b/test/live-models.test.mjs index f02c822..fff5d60 100644 --- a/test/live-models.test.mjs +++ b/test/live-models.test.mjs @@ -64,11 +64,32 @@ server.listen(TEST_PORT, "127.0.0.1", async () => { const spaceDuration = ((Date.now() - spaceStart) / 1000).toFixed(2); console.log(`āœ” Space Bunny responded in ${spaceDuration}s:`); console.log(` Output: "${spaceReply}"`); - console.log(` Usage: ${JSON.stringify(spaceJson.usage)}`); assert.ok(spaceReply && /yellow/i.test(spaceReply), "Space Bunny should correctly reply with yellow"); + // 3. Verify Reasoning Effort with Muse Spark 1.3 Contributor + console.log("\n[3/3] Verifying Configurable Reasoning Effort: Muse Spark 1.3 with reasoning_effort='high'..."); + const effortStart = Date.now(); + const effortRes = await fetch(`${baseUrl}/chat/completions`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + model: "muse-spark-1.3-contributor", + messages: [{ role: "user", content: "Is 97 prime? Reply yes or no only." }], + reasoning_effort: "high", + stream: false + }) + }); + + assert.equal(effortRes.status, 200, `Muse Spark with reasoning_effort should be 200 (was ${effortRes.status})`); + const effortJson = await effortRes.json(); + const effortReply = effortJson.choices?.[0]?.message?.content?.trim(); + const effortDuration = ((Date.now() - effortStart) / 1000).toFixed(2); + console.log(`āœ” Muse Spark with reasoning_effort='high' responded in ${effortDuration}s:`); + console.log(` Output: "${effortReply}"`); + assert.ok(effortReply && /yes/i.test(effortReply), "Muse Spark should answer yes for prime 97"); + console.log("\n=================================================="); - console.log("āœ” LIVE VERIFICATION PASSED FOR BOTH MODELS!"); + console.log("āœ” LIVE VERIFICATION PASSED FOR ALL MODELS & REASONING!"); console.log("=================================================="); } catch (err) { console.error("\nāŒ Live model verification failed:", err); diff --git a/test/translator.test.mjs b/test/translator.test.mjs index aa18f42..a9f4a6c 100644 --- a/test/translator.test.mjs +++ b/test/translator.test.mjs @@ -36,6 +36,39 @@ assert.equal(responsesPayload.input.length, 2); assert.equal(responsesPayload.input[0].role, "system"); assert.equal(responsesPayload.input[1].content, "Hello world"); +// 2b. Verify reasoning effort translation +const effortHighReq = { + model: "muse-spark-1.3-contributor", + messages: [{ role: "user", content: "Hi" }], + reasoning_effort: "high" +}; +const effortHighPayload = chatCompletionsToResponsesBody(effortHighReq); +assert.deepEqual(effortHighPayload.reasoning, { effort: "high" }); + +const effortXHighReq = { + model: "muse-spark-1.3-contributor", + messages: [{ role: "user", content: "Hi" }], + reasoning_effort: "xhigh" +}; +const effortXHighPayload = chatCompletionsToResponsesBody(effortXHighReq); +assert.deepEqual(effortXHighPayload.reasoning, { effort: "xhigh" }); + +const effortNoneReq = { + model: "muse-spark-1.3-contributor", + messages: [{ role: "user", content: "Hi" }], + reasoning_effort: "none" +}; +const effortNonePayload = chatCompletionsToResponsesBody(effortNoneReq); +assert.equal(effortNonePayload.reasoning, undefined); + +const effortNestedReq = { + model: "muse-spark-1.3-contributor", + messages: [{ role: "user", content: "Hi" }], + reasoning: { effort: "low" } +}; +const effortNestedPayload = chatCompletionsToResponsesBody(effortNestedReq); +assert.deepEqual(effortNestedPayload.reasoning, { effort: "low" }); + // 3. Responses API non-streaming JSON -> Chat Completions JSON const rawResponsesResp = { id: "resp_1234567890",