Skip to content

fix(codexmate): harden bridge retries and OpenClaw UX#207

Merged
ymkiux merged 13 commits into
mainfrom
fix/builtin-proxy-sub2api-hot-path
Jul 13, 2026
Merged

fix(codexmate): harden bridge retries and OpenClaw UX#207
ymkiux merged 13 commits into
mainfrom
fix/builtin-proxy-sub2api-hot-path

Conversation

@awsl233777

@awsl233777 awsl233777 commented Jun 27, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Harden the built-in OpenAI bridge so transient upstream failures keep retrying and post-header failures no longer leave Codex clients stuck in Working.
  • Complete converted streaming /v1/responses lifecycle events by emitting response.in_progress before completion.
  • Remove user-visible built-in transform retry-count controls from provider UI/config submission.
  • Improve the OpenClaw config tab with a compact config summary, AGENTS-style workspace file cards, and a cleaner SOUL/USER/TOOLS/HEARTBEAT editor flow.
  • Remove the redundant custom workspace-file input once the common workspace file cards cover the same entry points.

Changes

  • cli/openai-bridge.js
    • Avoids duplicate/late writeHead(500) calls when response headers/body state has already advanced.
    • Finishes or destroys already-started responses instead of leaving clients waiting.
    • Stops reading/submitting per-provider retry counts for the built-in transform path.
  • cli/openai-bridge-retry.js
    • Treats 408, 409, 425, 429, and all upstream 5xx statuses as transient retryable statuses.
    • Defaults transient bridge retries to continue until success or a non-transient result.
  • cli/openai-bridge-runtime.js
    • Adds response.in_progress to converted streaming Responses SSE output.
  • web-ui/*
    • Removes provider retry-count inputs and related validation/i18n/precompiled render output.
    • Fixes collapsed sidebar grid sizing.
    • Adds OpenClaw config summary cards for model, fallback, workspace, and browser state.
    • Converts OpenClaw workspace files into AGENTS-style cards and keeps the shared workspace editor flow.
    • Tightens OpenClaw workspace editor copy and removes the redundant manual workspace filename input.
  • tests/unit/*
    • Adds focused bridge retry/Responses regressions and OpenClaw config-summary coverage.

Validation

  • node --check cli/openai-bridge.js
  • node --check cli/openai-bridge-runtime.js
  • node --check cli/openai-bridge-retry.js
  • node --check web-ui/modules/app.methods.providers.mjs
  • node --check web-ui/app.js
  • node --test tests/unit/openai-bridge-upstream-responses.test.mjs — 33/33 pass
  • node tests/unit/run.mjs providers-validation provider-switch-regression provider-default-names web-ui-source-bundle — reports All 751 tests passed
  • npm run test:unit -- openclaw-core — reports All 753 tests passed
  • npm run lint — reports Lint passed for 233 file(s)
  • git diff --check

Notes

  • The OpenClaw tab remains a config/workspace-file surface; it does not read runtime chat/group messages.
  • The local unit runner still prints an existing search_sessions.py --limit -1 diagnostic after passing; exit code is 0 for the scoped runs.
  • No CodeRabbit review was requested as part of this update.

Summary by CodeRabbit

  • New Features
    • Added OpenClaw configuration summaries, status details, and quick-access workspace file buttons.
    • Improved workspace and agent editing layouts with clearer labels and responsive presentation.
    • Added built-in transform configuration toggle messaging across supported languages.
  • Bug Fixes
    • Improved retry handling for temporary upstream errors and connection failures.
    • Prevented secondary response errors after headers or streams have already closed.
    • Corrected collapsed sidebar layout behavior.
  • UI Improvements
    • Removed the configurable transform retry-count field from provider forms.
  • Release
    • Updated the application version to 0.1.2.

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The OpenAI bridge now retries transient upstream failures by default and safely handles post-response errors. Provider retry-count configuration was removed from bridge data and UI. OpenClaw summaries and quick-file controls were added, with related editor styling and collapsed sidebar sizing updates.

Changes

OpenAI bridge resilience

Layer / File(s) Summary
Transient upstream retry handling
cli/openai-bridge-retry.js, cli/openai-bridge.js, tests/unit/openai-bridge-upstream-responses.test.mjs
Uses unlimited default retries, recognizes transient HTTP statuses, removes provider-specific retry values, and tests network, JSON, and SSE retries.
Safe bridge error responses
cli/openai-bridge.js, tests/unit/openai-bridge-upstream-responses.test.mjs
Avoids duplicate error writes after headers are sent and destroys the response when defensive ending fails.
Responses SSE lifecycle
cli/openai-bridge-runtime.js, tests/unit/openai-bridge-upstream-responses.test.mjs
Initializes response.created with in-progress status and empty output, and verifies lifecycle events.

Provider transform retry removal

Layer / File(s) Summary
Provider state and request cleanup
web-ui/app.js, web-ui/modules/app.methods.providers.mjs, tests/unit/*provider*.test.mjs
Removes openaiBridgeMaxRetries from provider drafts, validation, payloads, local state, modal setup, resets, and expectations.
Provider transform UI updates
web-ui/partials/index/modals-basic.html, web-ui/res/web-ui-render.precompiled.js, web-ui/modules/i18n/locales/*.mjs
Removes retry-count inputs and replaces related locale strings with built-in-transform toggle text.

OpenClaw configuration summaries

Layer / File(s) Summary
OpenClaw summary methods
web-ui/modules/app.methods.openclaw-core.mjs, tests/unit/openclaw-core.test.mjs, tests/unit/web-ui-behavior-parity.test.mjs
Adds configuration parsing, status summaries, quick workspace files, file-opening behavior, and valid/invalid configuration tests.
OpenClaw summary rendering and styling
web-ui/partials/index/panel-config-openclaw.html, web-ui/res/web-ui-render.precompiled.js, web-ui/styles/openclaw-structured.css, web-ui/modules/app.methods.agents.mjs, web-ui/partials/index/modal-config-template-agents.html, web-ui/styles/modals-core.css
Renders summary cards, quick-file buttons, compact config summaries, context-aware workspace editor labels, and responsive styles.

Collapsed sidebar layout

Layer / File(s) Summary
Collapsed sidebar grid sizing
web-ui/styles/layout-shell.css
Applies the collapsed side-rail width to the shell grid’s first column.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant BridgeClient
  participant OpenAIBridge
  participant UpstreamEndpoint
  BridgeClient->>OpenAIBridge: request models or responses
  OpenAIBridge->>UpstreamEndpoint: send upstream request
  UpstreamEndpoint-->>OpenAIBridge: return transient status or network failure
  OpenAIBridge->>UpstreamEndpoint: retry request
  UpstreamEndpoint-->>OpenAIBridge: return successful response
  OpenAIBridge-->>BridgeClient: return JSON or converted SSE
Loading

Possibly related PRs

Suggested reviewers: solarhell, ymkiux, survivem

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the main changes: bridge retry hardening and OpenClaw UX improvements.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/builtin-proxy-sub2api-hot-path

Warning

Tools execution failed with the following error:

Failed to run tools: 13 INTERNAL: Received RST_STREAM with code 2 (Internal server error)


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cli/builtin-proxy.js (1)

143-151: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Retry the proxy’s own timeout errors.

Line 223 and Line 1632 emit error: 'timeout', but this classifier does not match that value, so retryTransientRequest stops after the first timed-out /chat/completions attempt.

Suggested fix
 function isTransientNetworkError(error) {
     const text = String(error || '').trim();
     if (!text) return false;
+    if (/timeout|timed out/i.test(text)) return true;
     if (/socket hang up/i.test(text)) return true;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/builtin-proxy.js` around lines 143 - 151, The transient error classifier
in isTransientNetworkError does not recognize the proxy’s own timeout marker, so
retryTransientRequest treats timed-out /chat/completions failures as
non-retryable. Update the timeout detection logic in isTransientNetworkError in
builtin-proxy.js to return true when the error text is the proxy’s emitted
timeout value (error: 'timeout'), alongside the existing socket/TLS/network
checks, so timeout failures are retried consistently.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@cli/builtin-proxy.js`:
- Around line 143-151: The transient error classifier in isTransientNetworkError
does not recognize the proxy’s own timeout marker, so retryTransientRequest
treats timed-out /chat/completions failures as non-retryable. Update the timeout
detection logic in isTransientNetworkError in builtin-proxy.js to return true
when the error text is the proxy’s emitted timeout value (error: 'timeout'),
alongside the existing socket/TLS/network checks, so timeout failures are
retried consistently.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 50a6be28-37d9-4f28-aeb1-ff1504bc3e36

📥 Commits

Reviewing files that changed from the base of the PR and between 3d4e522 and d8799a7.

📒 Files selected for processing (4)
  • cli/builtin-proxy.js
  • tests/e2e/run.js
  • tests/e2e/test-builtin-proxy-codex-conversion.js
  • tests/unit/builtin-proxy-responses-shim.test.mjs
📜 Review details
🧰 Additional context used
🪛 ast-grep (0.44.0)
tests/e2e/run.js

[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: require('child_process')
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process)


[warning] Importing child_process exposes a command-execution surface; ensure any command/argument built from input is validated, and prefer execFile/spawn with an argument array over exec.
Context: require('child_process')
Note: [CWE-78] Improper Neutralization of Special Elements used in an OS Command ('OS Command Injection').

(detect-child-process)

tests/e2e/test-builtin-proxy-codex-conversion.js

[warning] 37-130: Use https protocol over http
Context: http.createServer((req, res) => {
const requestPath = String(req.url || '').split('?')[0];
let body = '';
req.setEncoding('utf-8');
req.on('data', chunk => body += chunk);
req.on('end', () => {
let parsedBody = null;
if (body.trim()) {
try { parsedBody = JSON.parse(body); } catch (_) { parsedBody = null; }
}
requests.push({
method: req.method,
path: requestPath,
headers: req.headers,
body: parsedBody
});

            if (req.method === 'GET' && requestPath === '/nested/api/models') {
                const payload = JSON.stringify({ data: [{ id: 'gpt-sub2api-e2e' }] });
                res.writeHead(200, {
                    'Content-Type': 'application/json; charset=utf-8',
                    'Content-Length': Buffer.byteLength(payload, 'utf-8')
                });
                res.end(payload, 'utf-8');
                return;
            }

            if (req.method === 'POST' && requestPath === '/nested/api/responses') {
                const payload = JSON.stringify({ error: { message: 'responses endpoint must not be used' } });
                res.writeHead(500, {
                    'Content-Type': 'application/json; charset=utf-8',
                    'Content-Length': Buffer.byteLength(payload, 'utf-8')
                });
                res.end(payload, 'utf-8');
                return;
            }

            if (req.method === 'POST' && requestPath === '/nested/api/chat/completions') {
                if (parsedBody && parsedBody.model === 'error-model') {
                    const payload = JSON.stringify({ error: { message: 'upstream rate limited' } });
                    res.writeHead(429, {
                        'Content-Type': 'application/json; charset=utf-8',
                        'Content-Length': Buffer.byteLength(payload, 'utf-8')
                    });
                    res.end(payload, 'utf-8');
                    return;
                }

                if (parsedBody && parsedBody.stream === true) {
                    res.writeHead(200, { 'Content-Type': 'text/event-stream; charset=utf-8' });
                    res.write('data: {"id":"chatcmpl_sub2api_stream","model":"gpt-sub2api-e2e","choices":[{"delta":{"role":"assistant"}}]}\n\n');
                    res.write('data: {"id":"chatcmpl_sub2api_stream","model":"gpt-sub2api-e2e","choices":[{"delta":{"content":"stream-"}}]}\n\n');
                    res.write('data: {"id":"chatcmpl_sub2api_stream","model":"gpt-sub2api-e2e","choices":[{"delta":{"content":"ok"}}]}\n\n');
                    res.end('data: [DONE]\n\n');
                    return;
                }

                const hasTools = parsedBody && Array.isArray(parsedBody.tools) && parsedBody.tools.length > 0;
                const payload = JSON.stringify({
                    id: hasTools ? 'chatcmpl_sub2api_tool' : 'chatcmpl_sub2api_text',
                    model: parsedBody && parsedBody.model ? parsedBody.model : 'unknown-model',
                    choices: [{
                        finish_reason: hasTools ? 'tool_calls' : 'stop',
                        message: hasTools ? {
                            role: 'assistant',
                            content: null,
                            tool_calls: [{
                                id: 'call_lookup_e2e',
                                type: 'function',
                                function: { name: 'lookup', arguments: '{"q":"codexmate"}' }
                            }]
                        } : {
                            role: 'assistant',
                            content: 'direct-chat-ok'
                        }
                    }],
                    usage: { prompt_tokens: 5, completion_tokens: 7, total_tokens: 12 }
                });
                res.writeHead(200, {
                    'Content-Type': 'application/json; charset=utf-8',
                    'Content-Length': Buffer.byteLength(payload, 'utf-8')
                });
                res.end(payload, 'utf-8');
                return;
            }

            const notFound = JSON.stringify({ error: { message: 'not found' } });
            res.writeHead(404, {
                'Content-Type': 'application/json; charset=utf-8',
                'Content-Length': Buffer.byteLength(notFound, 'utf-8')
            });
            res.end(notFound, 'utf-8');
        });
    })

Note: [CWE-319] Cleartext Transmission of Sensitive Information. Security best practice.

(https-protocol-missing)

🔇 Additional comments (4)
tests/unit/builtin-proxy-responses-shim.test.mjs (1)

131-148: LGTM!

Also applies to: 168-169, 496-507, 542-551, 1390-1410, 1436-1440

tests/e2e/run.js (1)

19-19: LGTM!

Also applies to: 155-155

tests/e2e/test-builtin-proxy-codex-conversion.js (1)

178-179: 🎯 Functional Correctness

No change needed for the non-stream lookup The non-stream body already sets stream: false, so req.body.stream === false is safe.

cli/builtin-proxy.js (1)

957-959: LGTM!

Also applies to: 1642-1644, 2008-2012, 2037-2059, 2061-2070

@awsl233777

Copy link
Copy Markdown
Collaborator Author

Updated after CodeRabbit feedback:

  • Classified plain timeout errors as transient network failures so the proxy retries its own timeout results from upstream chat requests.
  • Added a regression test that simulates the first /v1/chat/completions attempt hanging until proxy timeout and verifies the second attempt recovers.

Validation after the update:

  • node --test tests/unit/builtin-proxy-responses-shim.test.mjs — 22/22 passed
  • npm run test:unit — All 713 tests passed, UNIT_EXIT=0
  • npm run test:e2eE2E_EXIT=0
  • npm run lint — lint passed for 229 files, LINT_EXIT=0
  • git diff --check — no whitespace errors

@coderabbitai re-review

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

@awsl233777 Thanks for the update and for sharing the validation results. I’ll re-review the latest changes now.

✅ Action performed

Review finished.

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

@awsl233777

Copy link
Copy Markdown
Collaborator Author

Updated the builtin Responses streaming path for the stability issue reported in chat:

  • Open downstream Responses SSE immediately before waiting for upstream /v1/chat/completions headers.
  • Emit early response.created with status: "in_progress" / output: [], followed by response.in_progress, matching the stable Responses stream shape expected by Codex-style clients.
  • Keep the downstream stream alive every 5s while waiting for upstream output.
  • Track client disconnects and abort upstream requests even while the proxy is still waiting for upstream headers.
  • Preserve retry behavior for pre-output upstream aborts/timeouts after the downstream SSE has already opened.
  • Added unit and e2e regressions where upstream intentionally delays streaming headers; the proxy must still emit response.created / response.in_progress within 500ms and must not call upstream /responses.

Validation after this update:

  • node --test tests/unit/builtin-proxy-responses-shim.test.mjs — 23/23 passed
  • npm run test:e2eE2E_EXIT=0
  • npm run test:unit — All 714 tests passed, UNIT_EXIT=0
  • npm run lint — lint passed for 229 files, LINT_EXIT=0
  • git diff --check — no whitespace errors

@coderabbitai re-review

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

@awsl233777 Thanks for the detailed update and validation results. I’ll review the latest changes to the Responses streaming path now, including the early SSE open, keepalive behavior, disconnect/abort handling, and the new regression coverage.

✅ Action performed

Review finished.

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

@awsl233777

Copy link
Copy Markdown
Collaborator Author

Updated the streaming bridge to align with sub2api's Chat Completions → Responses lifecycle instead of synthesizing a custom stream shape.

What changed:

  • Removed the custom response.in_progress event from chat fallback streaming.
  • Emit response.created from the first upstream chat stream chunk, including role-only chunks, matching sub2api's ensureChatToResponsesCreated behavior.
  • Added reasoning summary part lifecycle: response.reasoning_summary_part.added before reasoning deltas and response.reasoning_summary_part.done before closing the reasoning item.
  • Close reasoning before opening message/tool output, matching sub2api item ordering.
  • Emit message output_item.added + content_part.added before response.output_text.delta.
  • Emit tool-call output_item.added and streamed response.function_call_arguments.delta as argument chunks arrive, then aggregate in response.function_call_arguments.done and output_item.done at finalization.
  • Preserve retry behavior before any output item is started; JSON fallback now still emits a single response.created after retry recovery.
  • Updated the slow-stream regression to model sub2api behavior: first upstream role chunk arrives quickly, content is delayed, and the proxy emits response.created before delayed content without inventing response.in_progress.

Validation after this update:

  • node --test tests/unit/builtin-proxy-responses-shim.test.mjs — 23/23 passed
  • npm run test:e2eE2E_EXIT=0
  • npm run test:unit — All 714 tests passed, UNIT_EXIT=0
  • npm run lint — lint passed for 229 files, LINT_EXIT=0
  • git diff --check — no whitespace errors

@coderabbitai re-review

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

@awsl233777 Thanks for the detailed update and validation results. I’ll review the latest streaming bridge changes now, with focus on the sub2api-aligned Responses event lifecycle, early response.created behavior, reasoning/tool ordering, and retry semantics before output starts.


✏️ Learnings added
Learnt from: awsl233777
URL: https://github.com/SakuraByteCore/codexmate/pull/207

Timestamp: 2026-06-27T08:40:32.845Z
Learning: In the builtin proxy Responses streaming bridge for `cli/builtin-proxy.js`, the expected fallback behavior is to align with sub2api's Chat Completions → Responses lifecycle: do not synthesize a custom `response.in_progress` event, emit `response.created` from the first upstream chat stream chunk (including role-only chunks), and preserve sub2api-style ordering by finishing reasoning items before opening message or tool output items.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Action performed

Review finished.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/e2e/test-builtin-proxy-codex-conversion.js (1)

212-212: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Avoid picking the proxy port from a fixed random range.

20000 + Math.floor(Math.random() * 1000) can collide with another process in CI, so this test can fail before it exercises the proxy at all. Prefer an OS-assigned free port or reserve one explicitly first.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/test-builtin-proxy-codex-conversion.js` at line 212, The proxy port
selection in the e2e test is using a fixed random range, which can still collide
in CI. Update the test setup around the proxyPort assignment to obtain an
OS-assigned free port or reserve a port explicitly before starting the proxy,
and keep the change localized to the proxy test flow so the rest of the test
continues to use the chosen port.
🧹 Nitpick comments (2)
cli/builtin-proxy.js (1)

2244-2254: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Unreachable dead code: this if (wantsStream) block can never execute.

wantsStream is const (Line 2185) and the streaming case already returns inside the earlier if (wantsStream) { ... return; } block (Lines 2193-2215). By the time execution reaches Line 2244, wantsStream is always false, so this branch (and its sendResponsesSse/event-stream write) is dead. Drop it and keep only the JSON response.

♻️ Proposed cleanup
                     const responsesPayload = buildResponsesPayloadFromChatCompletion(chatJson.value, model, { toolTypesByName });
-
-                    if (wantsStream) {
-                        res.writeHead(200, {
-                            'Content-Type': 'text/event-stream; charset=utf-8',
-                            'Cache-Control': 'no-cache',
-                            'Connection': 'keep-alive',
-                            'X-Accel-Buffering': 'no'
-                        });
-                        sendResponsesSse(res, responsesPayload);
-                        res.end();
-                        return;
-                    }
                     res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
                     res.end(JSON.stringify(responsesPayload));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/builtin-proxy.js` around lines 2244 - 2254, Remove the unreachable
streaming branch in builtin-proxy.js: the later if (wantsStream) block around
the response handling is dead because wantsStream is a const and the earlier
streaming path already returns after sendResponsesSse. Update the response flow
so only the JSON response path remains there, and keep the logic centered around
the existing request handling code that computes wantsStream and
sendResponsesSse.
tests/e2e/test-builtin-proxy-codex-conversion.js (1)

276-285: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

The slow-stream regression stops before it verifies delayed output.

requestUntilText(..., 'event: response.created', ...) resolves on the first marker and requestUntilText destroys the request immediately afterward, so a proxy that emits response.created and then stalls forever would still pass here. Consider following this with a full-body read (or a helper that waits for both the early marker and [DONE]) so the delayed-content path is actually covered.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/test-builtin-proxy-codex-conversion.js` around lines 276 - 285, The
slow-stream test in requestUntilText only proves the early response.created
marker arrives and then cancels the request, so it never validates delayed
output. Update the test around earlyStreamResponse to keep reading after the
first marker or use a helper that waits for both the early SSE event and the
eventual [DONE] terminator, so the slow-content path is actually exercised and
verified.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/e2e/test-builtin-proxy-codex-conversion.js`:
- Line 49: The timed-out probe path in the E2E helper leaves the probe request
open, which can leak the SSE socket and flake later steps. Update the timeout
branch in the probe setup (the setTimeout that calls finish in
test-builtin-proxy-codex-conversion.js) to explicitly cancel/close the in-flight
req before resolving the timeout result, and make sure the cleanup happens for
the timeout path as well as normal completion.

In `@tests/unit/builtin-proxy-responses-shim.test.mjs`:
- Around line 1697-1699: The SSE assertions in the builtin proxy responses shim
test are too specific about how function-call arguments are chunked, so update
the test around the response.function_call_arguments.delta checks to verify the
full reconstructed arguments content instead of a particular delta split
pattern. Keep the event-name assertion in the relevant test case, but replace
the two delta-partition regex matches with assertions that tolerate any valid
re-chunking while still confirming the final JSON arguments for the function
call are correct.
- Around line 1222-1251: Loosen the timeout gate in the slow-stream test so it
matches the intended contract instead of failing too early. In the test around
the earlySse/proxyPort request flow, increase the setTimeout used to finish the
probe and keep the assertions focused on “response.created arrives before
delayed content” by checking the first upstream chat chunk behavior in
builtin-proxy-responses-shim.test.mjs. Update the elapsed-time expectation to
allow normal CI jitter while still ensuring the SSE is emitted before the
delayed content from the proxy path.

---

Outside diff comments:
In `@tests/e2e/test-builtin-proxy-codex-conversion.js`:
- Line 212: The proxy port selection in the e2e test is using a fixed random
range, which can still collide in CI. Update the test setup around the proxyPort
assignment to obtain an OS-assigned free port or reserve a port explicitly
before starting the proxy, and keep the change localized to the proxy test flow
so the rest of the test continues to use the chosen port.

---

Nitpick comments:
In `@cli/builtin-proxy.js`:
- Around line 2244-2254: Remove the unreachable streaming branch in
builtin-proxy.js: the later if (wantsStream) block around the response handling
is dead because wantsStream is a const and the earlier streaming path already
returns after sendResponsesSse. Update the response flow so only the JSON
response path remains there, and keep the logic centered around the existing
request handling code that computes wantsStream and sendResponsesSse.

In `@tests/e2e/test-builtin-proxy-codex-conversion.js`:
- Around line 276-285: The slow-stream test in requestUntilText only proves the
early response.created marker arrives and then cancels the request, so it never
validates delayed output. Update the test around earlyStreamResponse to keep
reading after the first marker or use a helper that waits for both the early SSE
event and the eventual [DONE] terminator, so the slow-content path is actually
exercised and verified.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ac286d6d-4360-408b-b256-a34f15b5c8d0

📥 Commits

Reviewing files that changed from the base of the PR and between 109c01b and bf47435.

📒 Files selected for processing (3)
  • cli/builtin-proxy.js
  • tests/e2e/test-builtin-proxy-codex-conversion.js
  • tests/unit/builtin-proxy-responses-shim.test.mjs
📜 Review details
🧰 Additional context used
🪛 ast-grep (0.44.0)
tests/e2e/test-builtin-proxy-codex-conversion.js

[warning] 48-48: Avoid using the initial state variable in setState
Context: setTimeout(() => finish({ statusCode: 0, body: text, firstDataElapsedMs, timedOut: true }), timeoutMs)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)

🔇 Additional comments (3)
cli/builtin-proxy.js (3)

143-153: LGTM!


1342-1419: LGTM!


1487-1533: LGTM!

Comment thread tests/e2e/test-builtin-proxy-codex-conversion.js Outdated
Comment thread tests/unit/builtin-proxy-responses-shim.test.mjs Outdated
Comment thread tests/unit/builtin-proxy-responses-shim.test.mjs Outdated
@awsl233777

Copy link
Copy Markdown
Collaborator Author

Stability follow-up pushed in 331db16.

What changed:

  • Opens the downstream SSE transport as soon as an upstream chat SSE stream is accepted, and sends a legal : keepalive while waiting for the first upstream chat chunk.
  • Preserves the sub2api event lifecycle: no synthetic response.in_progress event, and no response.created until the first upstream chat chunk arrives.
  • Adds accident-focused coverage for idle-before-first-chat-chunk, client aborts, full delayed completion, CPU delta, and upstream stream leak checks.

Validation actually run:

  • node --test tests/unit/builtin-proxy-responses-shim.test.mjs — 24/24 passed, TARGET_UNIT_EXIT=0
  • npm run test:e2eE2E_EXIT=0
    • stability log: elapsed_ms=808 cpu_user_us=10912 cpu_system_us=3683 metrics={"opened":14,"closed":14,"active":0,"maxActive":9}
  • npm run test:unit — All 715 tests passed, UNIT_EXIT=0
  • npm run lint — lint passed for 229 files, LINT_EXIT=0
  • git diff --checkDIFF_CHECK_EXIT=0

@coderabbitai re-review

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

@awsl233777 Thanks for the detailed follow-up and the validation results. I’ll re-review the latest changes in 331db16, with focus on the early SSE transport open/keepalive behavior, preserving the sub2api event lifecycle, and the new abort/leak/stability coverage.


✏️ Learnings added
Learnt from: awsl233777
URL: https://github.com/SakuraByteCore/codexmate/pull/207

Timestamp: 2026-06-27T10:07:29.883Z
Learning: In `cli/builtin-proxy.js`, the builtin Responses streaming bridge is expected to open the downstream SSE transport as soon as the upstream chat SSE stream is accepted and may send legal `: keepalive` comments while idle, but it must still preserve the sub2api lifecycle by not emitting synthetic `response.in_progress` and not emitting `response.created` until the first upstream chat stream chunk arrives.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Action performed

Review finished.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/unit/builtin-proxy-responses-shim.test.mjs (1)

1311-1315: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Assert this idle-stream path never probes /v1/responses.

The handler returns 404 but does not record/assert usage, so this test could still pass if the proxy probes /v1/responses and then falls back to chat.

Proposed test tightening
+    let responsesHit = false;
     const upstream = http.createServer((req, res) => {
         if (req.url === '/v1/responses' && req.method === 'POST') {
+            responsesHit = true;
             res.writeHead(404, { 'Content-Type': 'application/json' });
             res.end(JSON.stringify({ error: 'responses endpoint unavailable' }));
             return;
@@
         assert.match(early.text, /: keepalive/);
         assert.doesNotMatch(early.text, /event: response\.created/);
         assert.doesNotMatch(early.text, /event: response\.in_progress/);
+        assert.equal(responsesHit, false, 'proxy should not probe upstream /v1/responses before chat streaming');

Also applies to: 1376-1381

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/builtin-proxy-responses-shim.test.mjs` around lines 1311 - 1315,
The idle-stream fallback test is not verifying that the proxy avoids probing
/v1/responses, so it can pass even if the proxy still makes that request before
falling back. Update the upstream test server in
builtin-proxy-responses-shim.test.mjs to explicitly record or assert any
/v1/responses POST attempts in the relevant idle-stream cases, and then fail the
test if that path is hit; use the existing upstream request handler around the
idle-stream scenarios so the assertion is tied to the proxy behavior being
tested.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/unit/builtin-proxy-responses-shim.test.mjs`:
- Around line 1350-1351: The early probe timeout path in the keepalive test
leaves the HTTP request open, so update the timeout handling around the
timer/http.request probe to explicitly destroy or abort the request before
calling finish when the 500ms timeout fires. Make the fix in the probe setup
logic so the request lifecycle is cleaned up on timeout, and ensure the existing
finish callback still reports the timedOut result only after the request has
been torn down.

---

Nitpick comments:
In `@tests/unit/builtin-proxy-responses-shim.test.mjs`:
- Around line 1311-1315: The idle-stream fallback test is not verifying that the
proxy avoids probing /v1/responses, so it can pass even if the proxy still makes
that request before falling back. Update the upstream test server in
builtin-proxy-responses-shim.test.mjs to explicitly record or assert any
/v1/responses POST attempts in the relevant idle-stream cases, and then fail the
test if that path is hit; use the existing upstream request handler around the
idle-stream scenarios so the assertion is tied to the proxy behavior being
tested.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d4acf102-9f71-40ae-a612-a442249b8c62

📥 Commits

Reviewing files that changed from the base of the PR and between bf47435 and 331db16.

📒 Files selected for processing (3)
  • cli/builtin-proxy.js
  • tests/e2e/test-builtin-proxy-codex-conversion.js
  • tests/unit/builtin-proxy-responses-shim.test.mjs
🚧 Files skipped from review as they are similar to previous changes (1)
  • cli/builtin-proxy.js
📜 Review details
🧰 Additional context used
🪛 ast-grep (0.44.0)
tests/e2e/test-builtin-proxy-codex-conversion.js

[warning] 83-83: Avoid using the initial state variable in setState
Context: setTimeout(resolve, ms)
Note: [CWE-710] Improper Adherence to Coding Standards. Security best practice.

(setstate-same-var)

🔇 Additional comments (2)
tests/e2e/test-builtin-proxy-codex-conversion.js (1)

83-117: LGTM!

Also applies to: 170-191, 245-245, 333-390

tests/unit/builtin-proxy-responses-shim.test.mjs (1)

1310-1310: LGTM!

Also applies to: 1316-1349, 1352-1388

Comment thread tests/unit/builtin-proxy-responses-shim.test.mjs Outdated
@awsl233777

Copy link
Copy Markdown
Collaborator Author

Addressed the latest CodeRabbit test-stability feedback in 3d70475.

What changed:

  • Destroy timeout probes in both unit and e2e helpers so missed markers do not leave HTTP/SSE requests open.
  • Use an OS-assigned free port for the e2e builtin proxy instead of a fixed random range.
  • Assert the idle-before-first-chat-chunk unit path does not probe upstream /v1/responses.
  • Reconstruct streamed function-call argument deltas instead of asserting one specific chunk split.
  • Loosen the slow-stream wall-clock guard to the actual contract: early SSE before the 1200ms delayed content.

Validation actually run:

  • node --test tests/unit/builtin-proxy-responses-shim.test.mjs — 24/24 passed, TARGET_UNIT_EXIT=0
  • npm run test:e2eE2E_EXIT=0
    • stability log: elapsed_ms=812 cpu_user_us=11920 cpu_system_us=2588 metrics={"opened":14,"closed":14,"active":0,"maxActive":4}
  • npm run test:unit — All 715 tests passed, UNIT_EXIT=0
  • npm run lint — lint passed for 229 files, LINT_EXIT=0
  • git diff --checkDIFF_CHECK_EXIT=0

@coderabbitai re-review

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

@awsl233777 Thanks for the follow-up and the detailed validation results. I’ll re-review 3d70475 with focus on the test-stability fixes: timeout-probe cleanup, OS-assigned port usage, the no-/v1/responses idle-stream assertion, function-call delta reconstruction, and the relaxed slow-stream timing contract.

(•ᴗ•)🐇


✏️ Learnings added
Learnt from: awsl233777
URL: https://github.com/SakuraByteCore/codexmate/pull/207

Timestamp: 2026-06-27T10:16:47.876Z
Learning: In `tests/unit/builtin-proxy-responses-shim.test.mjs` for the builtin proxy Responses streaming bridge, streamed `response.function_call_arguments.delta` chunks should be treated as arbitrarily split; tests should reconstruct the full arguments payload instead of asserting a specific chunk boundary pattern.

Learnt from: awsl233777
URL: https://github.com/SakuraByteCore/codexmate/pull/207

Timestamp: 2026-06-27T10:16:47.876Z
Learning: In the builtin proxy end-to-end tests for this repository, the e2e proxy listener should use an OS-assigned free port instead of choosing from a fixed random range to reduce test flakiness and port-collision risk.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Action performed

Review finished.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
tests/e2e/test-builtin-proxy-codex-conversion.js (2)

372-407: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Assert the CPU guard instead of only logging it.

This computes process.cpuUsage() but never fails on a busy-loop regression, so the claimed CPU stability coverage is observational only. Add a generous ceiling or remove the metric from the test contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/test-builtin-proxy-codex-conversion.js` around lines 372 - 407, The
stability test in the idle stream flow currently records CPU usage but only logs
it, so it won’t fail on a busy-loop regression. In the test block around the
existing process.cpuUsage() measurement and the final console.log, add an
assertion on the CPU delta (using the stabilityCpu values from the idle stream
run) with a generous ceiling, or otherwise drop CPU usage from the test contract
entirely. Keep the check aligned with the existing idle-stream assertions in
this test so the CPU guard is enforced by the same stability scenario.

86-99: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Avoid the port handoff race
getAvailableTcpPort() closes the socket before proxy-start binds it, leaving a TOCTOU window in both uses here and at 286-294. proxy-start also rejects port: 0, so the fix needs to move to the startup path if you want to remove the flake.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/e2e/test-builtin-proxy-codex-conversion.js` around lines 86 - 99, The
race is in getAvailableTcpPort and the proxy-start setup: the helper closes the
temporary server before proxy-start can bind, creating a TOCTOU window, and
proxy-start currently rejects port 0. Update the startup path around proxy-start
so it acquires and binds a real available port directly instead of handing off a
released socket, and adjust the getAvailableTcpPort usage so the selected port
stays reserved until the server starts. Use the getAvailableTcpPort helper and
proxy-start port handling as the main symbols to locate the changes.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/e2e/test-builtin-proxy-codex-conversion.js`:
- Around line 372-407: The stability test in the idle stream flow currently
records CPU usage but only logs it, so it won’t fail on a busy-loop regression.
In the test block around the existing process.cpuUsage() measurement and the
final console.log, add an assertion on the CPU delta (using the stabilityCpu
values from the idle stream run) with a generous ceiling, or otherwise drop CPU
usage from the test contract entirely. Keep the check aligned with the existing
idle-stream assertions in this test so the CPU guard is enforced by the same
stability scenario.
- Around line 86-99: The race is in getAvailableTcpPort and the proxy-start
setup: the helper closes the temporary server before proxy-start can bind,
creating a TOCTOU window, and proxy-start currently rejects port 0. Update the
startup path around proxy-start so it acquires and binds a real available port
directly instead of handing off a released socket, and adjust the
getAvailableTcpPort usage so the selected port stays reserved until the server
starts. Use the getAvailableTcpPort helper and proxy-start port handling as the
main symbols to locate the changes.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: ad0a9d66-9dac-4e57-b135-5900a23935fe

📥 Commits

Reviewing files that changed from the base of the PR and between 331db16 and 3d70475.

📒 Files selected for processing (2)
  • tests/e2e/test-builtin-proxy-codex-conversion.js
  • tests/unit/builtin-proxy-responses-shim.test.mjs
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/unit/builtin-proxy-responses-shim.test.mjs
📜 Review details
🧰 Additional context used
🪛 ast-grep (0.44.0)
tests/e2e/test-builtin-proxy-codex-conversion.js

[warning] 87-87: Use https protocol over http
Context: http.createServer()
Note: [CWE-319] Cleartext Transmission of Sensitive Information. Security best practice.

(https-protocol-missing)

🔇 Additional comments (1)
tests/e2e/test-builtin-proxy-codex-conversion.js (1)

49-52: LGTM!

@awsl233777

Copy link
Copy Markdown
Collaborator Author

事故复现链路这轮又补了一刀,不按 review nitpick 走:

  • 修复 cli/builtin-proxy.js 里 Chat fallback streaming 的 DSML 工具调用泄漏:当上游把 <|DSML|tool_calls> 放进 delta.content 时,不再把内部工具调用标记作为 response.output_text.delta 透给 Codex/UI。
  • 新增内容 buffer + DSML parser:普通文本继续流式输出;完整 DSML tool-call block 会转换成 Responses function_call / response.function_call_arguments.* 事件。
  • 覆盖 SSE chunk 切分场景:新增单测模拟 <|DSML|tool_calls> 被切成两段 content chunk,断言最终 SSE 不含 DSML / invoke name,并正确生成 read 工具调用和参数。

已验证:

node --check cli/builtin-proxy.js
node --check tests/unit/builtin-proxy-responses-shim.test.mjs
node --test tests/unit/builtin-proxy-responses-shim.test.mjs   # 25/25, TARGET_UNIT_EXIT=0
npm run test:e2e                                             # E2E_EXIT=0
npm run test:unit                                            # 716 tests passed, UNIT_EXIT=0
npm run lint                                                 # LINT_EXIT=0
git diff --check                                             # DIFF_CHECK_EXIT=0

说明:这次修的是用户现场看到的 “DSML/tool_calls 内脏直接出现在正文 + Working 卡住” 这一类真实事故路径,不是只为了让 CI 变绿。

@awsl233777

Copy link
Copy Markdown
Collaborator Author

Update: fixed the real --bridge openai path as well.

Changes in this round:

  • Added DSML tool-call parsing/conversion to cli/openai-bridge.js chat fallback streaming.
  • Buffered delta.content so split DSML markers like <|DSM + L|tool_calls> do not leak as output text.
  • Converted parsed DSML invokes into Responses function-call events instead of response.output_text.delta.
  • Normalized wrapped path-like DSML parameter values, e.g. /data/data/.../home/\n codexmate.
  • Extended both builtin-proxy and openai-bridge unit coverage for:
    • multiple invokes in one DSML block
    • parameter attributes split across lines
    • path values split across lines
    • DSML marker split across SSE chunks
    • no DSML / invoke name text leaking into emitted SSE

Validation actually run:

  • node --check cli/openai-bridge.js
  • node --check tests/unit/openai-bridge-upstream-responses.test.mjs
  • node --test tests/unit/openai-bridge-upstream-responses.test.mjs — 27/27 passed
  • node --test tests/unit/builtin-proxy-responses-shim.test.mjs tests/unit/openai-bridge-upstream-responses.test.mjs — 52/52 passed
  • npm run test:e2e — passed
  • npm run test:unit — 716 tests passed
  • npm run lint — passed for 229 files
  • git diff --check — passed

Real provider re-test boundary:

  • Re-ran codex exec through local http://127.0.0.1:3737/bridge/openai/1/v1/responses after restarting the runtime with this code.
  • The second real run did not first reproduce the DSML leak, but it was stopped by upstream provider error: 403 Forbidden: Insufficient balance.
  • Therefore I am not claiming the full provider-1 complex Codex task completed end-to-end; it is currently blocked by upstream balance/quota.

@awsl233777

Copy link
Copy Markdown
Collaborator Author

Follow-up after real provider UX review: added bounded bridge-side retries for early streaming failures.

Why:

  • The real --bridge openai path can receive intermittent upstream failures such as 403 Forbidden: Insufficient balance before any usable SSE output starts.
  • Passing that raw HTTP status back to Codex makes the CLI show a slow Reconnecting... 1/10 loop, which is poor UX and obscures the actual provider error.

Changes in this round:

  • Added retry classification for early streaming failures in cli/openai-bridge.js:
    • transient network/timeout errors
    • 408 / 409 / 425 / 429
    • 5xx
    • 403 bodies that look like provider-pool quota/rate/temporary failures, including Insufficient balance
  • Retries are bounded by the existing retry delays before any downstream SSE output is committed.
  • If retries still fail, the bridge now returns a proper Responses SSE response.failed + [DONE] instead of a raw HTTP 403/502 to the Codex SSE client.
  • Once the upstream stream has been accepted/output has started, the bridge does not attempt blind replay, avoiding duplicated output/tool calls.

Regression coverage added/updated:

  • Hanging upstream /responses does not fall back to chat; it ends as SSE response.failed instead of raw 502.
  • Upstream /responses 403 Insufficient balance is retried three attempts, then returned as SSE response.failed.
  • Upstream /responses first 403 then success recovers and emits response.completed without response.failed.
  • Chat fallback 403 Insufficient balance is also retried before SSE failure.

Validation actually run:

  • node --check cli/openai-bridge.js
  • node --check tests/unit/openai-bridge-upstream-responses.test.mjs
  • node --test tests/unit/openai-bridge-upstream-responses.test.mjs — 30/30 passed
  • node --test tests/unit/builtin-proxy-responses-shim.test.mjs tests/unit/openai-bridge-upstream-responses.test.mjs — 55/55 passed
  • npm run test:e2e — passed
  • npm run test:unit — 716 tests passed
  • npm run lint — passed for 229 files
  • git diff --check — passed

Boundary:

  • This does not claim to fix the upstream provider's quota/balance instability.
  • It makes codexmate handle that instability more cleanly: retry briefly inside the bridge, then fail fast and explicitly if the provider still rejects.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
cli/builtin-proxy.js (3)

1123-1133: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Keep response.created in-progress and empty.

This emits the completed status and full output in response.created, before the later response.output_item.added events. That breaks the streamed lifecycle ordering this shim is trying to preserve; mirror beginChatStreamResponsesSse and only expose final output in response.completed.

Proposed fix
         if (!options.skipCreated) {
             writeSse(res, 'response.created', {
                 type: 'response.created',
                 response: {
                     id: responseId,
                     model,
                     created_at: response.created_at,
-                    status: response.status || 'in_progress',
-                    output: Array.isArray(response.output) ? response.output : []
+                    status: 'in_progress',
+                    output: []
                 }
             });
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/builtin-proxy.js` around lines 1123 - 1133, The `response.created` event
in the proxy stream is leaking completed state too early; adjust the `writeSse`
payload in `cli/builtin-proxy.js` so `response.created` stays in-progress and
empty, matching `beginChatStreamResponsesSse`. Keep `status` fixed to the
in-progress value and omit the full `output` here, then reserve the final status
and output population for `response.completed` only.

1684-1691: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Attach the client-close abort handler before waiting for upstream headers.

The close listener is only installed after upstream SSE is accepted. If the client disconnects during DNS/TLS/header wait, the upstream request remains alive until timeout.

Proposed fix
             });
             sharedState.upstreamReq = req;
+            if (res && typeof res.on === 'function' && !sharedState.closeListenerAttached) {
+                sharedState.closeListenerAttached = true;
+                res.on('close', () => {
+                    sharedState.clientClosed = true;
+                    stopChatStreamHeartbeat(sharedState);
+                    if (!sharedState.finished && sharedState.upstreamReq) {
+                        try { sharedState.upstreamReq.destroy(new Error('client aborted')); } catch (_) {}
+                    }
+                });
+            }
             req.setTimeout(timeoutMs, () => {
                 if (streamAccepted) return;
                 try { req.destroy(new Error('timeout')); } catch (_) {}
                 finish({ ok: false, error: 'timeout' });
             });
-            req.on('error', (err) => finish({ ok: false, error: err && err.message ? err.message : 'request failed' }));
+            req.on('error', (err) => {
+                if (sharedState.clientClosed || res.writableEnded || res.destroyed) {
+                    finish({ ok: true });
+                    return;
+                }
+                finish({ ok: false, error: err && err.message ? err.message : 'request failed' });
+            });

Also applies to: 1922-1929

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/builtin-proxy.js` around lines 1684 - 1691, The client-close abort
handler is attached too late in the streaming flow, so disconnects during
DNS/TLS/header waiting can leave the upstream request running. Move the
res.close listener setup in the relevant streaming path(s) in
cli/builtin-proxy.js so it is installed before awaiting upstream headers, and
ensure the existing state.clientClosed, stopChatStreamHeartbeat, and
state.upstreamReq.destroy('client aborted') logic still runs when the client
disconnects.

2335-2343: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Retry retryable upstream HTTP failures before surfacing the stream error.

The streaming route now relies on this chat-completions path, but status >= 400 results are returned with status, and retryTransientRequest stops on any positive status. That means 429/5xx and quota-style 403 failures before output are not retried.

Proposed fix
+    function isRetryableUpstreamStatus(status, bodyText = '') {
+        if (status === 408 || status === 409 || status === 425 || status === 429) return true;
+        if (status >= 500 && status <= 599) return true;
+        if (status === 403 && /quota|balance|credit|capacity|rate/i.test(String(bodyText || ''))) return true;
+        return false;
+    }
+
     async function retryTransientRequest(executor) {
         let lastResult = null;
         for (let attempt = 0; attempt <= TRANSIENT_RETRY_DELAYS_MS.length; attempt += 1) {
@@
             if (!result) return result;
             if (result.ok) return result;
             if (result.retry) return result;
-            if (result.status && result.status > 0) return result;
-            if (!result.retryTransient && !isTransientNetworkError(result.error)) return result;
+            if (result.retryTransient || isTransientNetworkError(result.error)) continue;
+            if (result.status && result.status > 0) return result;
         }
         return lastResult;
     }
                 if (status >= 400) {
                     upstreamRes.on('data', (chunk) => chunk && chunks.push(chunk));
-                    upstreamRes.on('end', () => finish({ ok: false, status, bodyText: chunks.length ? Buffer.concat(chunks).toString('utf-8') : '' }));
+                    upstreamRes.on('end', () => {
+                        const bodyText = chunks.length ? Buffer.concat(chunks).toString('utf-8') : '';
+                        finish({
+                            ok: false,
+                            status,
+                            bodyText,
+                            ...(isRetryableUpstreamStatus(status, bodyText) ? { retryTransient: true } : {})
+                        });
+                    });
                     return;
                 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/builtin-proxy.js` around lines 2335 - 2343, The streaming
chat-completions path is surfacing retryable upstream errors too early because
`streamChatCompletionsAsResponsesSseWithFallbackUrls` returns positive `status`
values that cause `retryTransientRequest` to stop immediately. Update the retry
handling around the `streamChatCompletionsAsResponsesSseWithFallbackUrls` call
in `builtin-proxy` so 429, 5xx, and quota-style 403 responses before any
streamed output are treated as transient and retried before failing the request.
Keep the fix localized to the streaming/chat-completions flow and ensure the
retry decision uses the upstream response status and body/error details
appropriately.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cli/openai-bridge.js`:
- Around line 1539-1574: The DSML buffering in
flushChatStreamContentBuffer/writeChatStreamContentSegment can grow without
bound when DSML_TOOL_CALLS_END never arrives, so add a maxBytes-based guard for
pending content. In flushChatStreamContentBuffer, track the buffered DSML block
size and trim or reject oversized incomplete blocks instead of keeping the full
buffer in state.contentBuffer; ensure the guard applies both when start/end
markers are missing and when buffering through writeChatStreamContentSegment.
Use the existing state fields and DSML marker handling to locate the change.

In `@tests/unit/openai-bridge-upstream-responses.test.mjs`:
- Around line 168-170: Wrap the shutdown logic in the
openai-bridge-upstream-responses test so it waits for server close completion
instead of just calling http.Server.close() directly. Update the cleanup around
bridge.close() and upstream.close() to use promise-based shutdown handling (for
example via a helper that resolves on the close callback) before removing
tmpDir. This should be done in the test cleanup path where bridge and upstream
are torn down.

---

Outside diff comments:
In `@cli/builtin-proxy.js`:
- Around line 1123-1133: The `response.created` event in the proxy stream is
leaking completed state too early; adjust the `writeSse` payload in
`cli/builtin-proxy.js` so `response.created` stays in-progress and empty,
matching `beginChatStreamResponsesSse`. Keep `status` fixed to the in-progress
value and omit the full `output` here, then reserve the final status and output
population for `response.completed` only.
- Around line 1684-1691: The client-close abort handler is attached too late in
the streaming flow, so disconnects during DNS/TLS/header waiting can leave the
upstream request running. Move the res.close listener setup in the relevant
streaming path(s) in cli/builtin-proxy.js so it is installed before awaiting
upstream headers, and ensure the existing state.clientClosed,
stopChatStreamHeartbeat, and state.upstreamReq.destroy('client aborted') logic
still runs when the client disconnects.
- Around line 2335-2343: The streaming chat-completions path is surfacing
retryable upstream errors too early because
`streamChatCompletionsAsResponsesSseWithFallbackUrls` returns positive `status`
values that cause `retryTransientRequest` to stop immediately. Update the retry
handling around the `streamChatCompletionsAsResponsesSseWithFallbackUrls` call
in `builtin-proxy` so 429, 5xx, and quota-style 403 responses before any
streamed output are treated as transient and retried before failing the request.
Keep the fix localized to the streaming/chat-completions flow and ensure the
retry decision uses the upstream response status and body/error details
appropriately.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 6fac4490-dbfd-4522-a47c-1301015e22ed

📥 Commits

Reviewing files that changed from the base of the PR and between 3d70475 and 6b5dcc2.

📒 Files selected for processing (4)
  • cli/builtin-proxy.js
  • cli/openai-bridge.js
  • tests/unit/builtin-proxy-responses-shim.test.mjs
  • tests/unit/openai-bridge-upstream-responses.test.mjs
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/unit/builtin-proxy-responses-shim.test.mjs
📜 Review details
🧰 Additional context used
🪛 OpenGrep (1.23.0)
cli/openai-bridge.js

[ERROR] 1451-1451: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)


[ERROR] 1476-1476: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)


[ERROR] 1484-1484: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

cli/builtin-proxy.js

[ERROR] 1259-1259: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)


[ERROR] 1284-1284: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)


[ERROR] 1292-1292: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

🔇 Additional comments (5)
tests/unit/openai-bridge-upstream-responses.test.mjs (1)

156-166: LGTM!

Also applies to: 173-227, 234-290, 297-351, 610-680

cli/openai-bridge.js (3)

1134-1151: LGTM!

Also applies to: 1251-1304


1413-1537: LGTM!


1618-1623: LGTM!

Also applies to: 1641-1641, 1865-1865, 2402-2402, 2413-2413, 2440-2442

cli/builtin-proxy.js (1)

143-153: LGTM!

Also applies to: 1057-1078, 1221-1239, 1242-1301, 1403-1497, 1519-1538, 1550-1563, 1568-1650, 1668-1717, 1805-1819, 1855-1872, 1880-1902, 2304-2308

Comment thread cli/openai-bridge.js Outdated
Comment on lines +1539 to +1574
function flushChatStreamContentBuffer(state, force = false) {
if (!state || typeof state.contentBuffer !== 'string' || !state.contentBuffer) return;
let buffer = state.contentBuffer;
state.contentBuffer = '';
while (buffer) {
const start = buffer.indexOf(DSML_TOOL_CALLS_START);
if (start < 0) {
const keep = force ? 0 : longestDsmlStartPrefixSuffix(buffer);
const text = keep > 0 ? buffer.slice(0, buffer.length - keep) : buffer;
emitChatStreamTextDelta(state, text);
state.contentBuffer = keep > 0 ? buffer.slice(buffer.length - keep) : '';
return;
}
if (start > 0) {
emitChatStreamTextDelta(state, buffer.slice(0, start));
buffer = buffer.slice(start);
}
const end = buffer.indexOf(DSML_TOOL_CALLS_END, DSML_TOOL_CALLS_START.length);
if (end < 0) {
if (force) {
state.contentBuffer = '';
} else {
state.contentBuffer = buffer;
}
return;
}
const blockText = buffer.slice(DSML_TOOL_CALLS_START.length, end);
appendDsmlToolCallsToChatStreamState(state, blockText);
buffer = buffer.slice(end + DSML_TOOL_CALLS_END.length);
}
}

function writeChatStreamContentSegment(state, segment) {
if (!state || typeof segment !== 'string' || !segment) return;
state.contentBuffer = (typeof state.contentBuffer === 'string' ? state.contentBuffer : '') + segment;
flushChatStreamContentBuffer(state, false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Bound incomplete DSML buffers.

Line 1561 retains the entire pending DSML block until the end marker arrives. Since successful streaming chunks are not capped by maxBytes, a malformed upstream stream can grow state.contentBuffer indefinitely and stall output.

Proposed guard
 const DSML_TOOL_CALLS_START = '<|DSML|tool_calls>';
 const DSML_TOOL_CALLS_END = '</|DSML|tool_calls>';
+const DSML_CONTENT_BUFFER_MAX_BYTES = 1024 * 1024;
@@
         if (end < 0) {
             if (force) {
                 state.contentBuffer = '';
             } else {
+                if (Buffer.byteLength(buffer, 'utf8') > DSML_CONTENT_BUFFER_MAX_BYTES) {
+                    state.contentBuffer = '';
+                    failChatStreamResponsesSse(state, 'DSML tool-call block exceeded buffer limit');
+                    return;
+                }
                 state.contentBuffer = buffer;
             }
             return;
         }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
function flushChatStreamContentBuffer(state, force = false) {
if (!state || typeof state.contentBuffer !== 'string' || !state.contentBuffer) return;
let buffer = state.contentBuffer;
state.contentBuffer = '';
while (buffer) {
const start = buffer.indexOf(DSML_TOOL_CALLS_START);
if (start < 0) {
const keep = force ? 0 : longestDsmlStartPrefixSuffix(buffer);
const text = keep > 0 ? buffer.slice(0, buffer.length - keep) : buffer;
emitChatStreamTextDelta(state, text);
state.contentBuffer = keep > 0 ? buffer.slice(buffer.length - keep) : '';
return;
}
if (start > 0) {
emitChatStreamTextDelta(state, buffer.slice(0, start));
buffer = buffer.slice(start);
}
const end = buffer.indexOf(DSML_TOOL_CALLS_END, DSML_TOOL_CALLS_START.length);
if (end < 0) {
if (force) {
state.contentBuffer = '';
} else {
state.contentBuffer = buffer;
}
return;
}
const blockText = buffer.slice(DSML_TOOL_CALLS_START.length, end);
appendDsmlToolCallsToChatStreamState(state, blockText);
buffer = buffer.slice(end + DSML_TOOL_CALLS_END.length);
}
}
function writeChatStreamContentSegment(state, segment) {
if (!state || typeof segment !== 'string' || !segment) return;
state.contentBuffer = (typeof state.contentBuffer === 'string' ? state.contentBuffer : '') + segment;
flushChatStreamContentBuffer(state, false);
const DSML_CONTENT_BUFFER_MAX_BYTES = 1024 * 1024;
function flushChatStreamContentBuffer(state, force = false) {
if (!state || typeof state.contentBuffer !== 'string' || !state.contentBuffer) return;
let buffer = state.contentBuffer;
state.contentBuffer = '';
while (buffer) {
const start = buffer.indexOf(DSML_TOOL_CALLS_START);
if (start < 0) {
const keep = force ? 0 : longestDsmlStartPrefixSuffix(buffer);
const text = keep > 0 ? buffer.slice(0, buffer.length - keep) : buffer;
emitChatStreamTextDelta(state, text);
state.contentBuffer = keep > 0 ? buffer.slice(buffer.length - keep) : '';
return;
}
if (start > 0) {
emitChatStreamTextDelta(state, buffer.slice(0, start));
buffer = buffer.slice(start);
}
const end = buffer.indexOf(DSML_TOOL_CALLS_END, DSML_TOOL_CALLS_START.length);
if (end < 0) {
if (force) {
state.contentBuffer = '';
} else {
if (Buffer.byteLength(buffer, 'utf8') > DSML_CONTENT_BUFFER_MAX_BYTES) {
state.contentBuffer = '';
failChatStreamResponsesSse(state, 'DSML tool-call block exceeded buffer limit');
return;
}
state.contentBuffer = buffer;
}
return;
}
const blockText = buffer.slice(DSML_TOOL_CALLS_START.length, end);
appendDsmlToolCallsToChatStreamState(state, blockText);
buffer = buffer.slice(end + DSML_TOOL_CALLS_END.length);
}
}
function writeChatStreamContentSegment(state, segment) {
if (!state || typeof segment !== 'string' || !segment) return;
state.contentBuffer = (typeof state.contentBuffer === 'string' ? state.contentBuffer : '') + segment;
flushChatStreamContentBuffer(state, false);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/openai-bridge.js` around lines 1539 - 1574, The DSML buffering in
flushChatStreamContentBuffer/writeChatStreamContentSegment can grow without
bound when DSML_TOOL_CALLS_END never arrives, so add a maxBytes-based guard for
pending content. In flushChatStreamContentBuffer, track the buffered DSML block
size and trim or reject oversized incomplete blocks instead of keeping the full
buffer in state.contentBuffer; ensure the guard applies both when start/end
markers are missing and when buffering through writeChatStreamContentSegment.
Use the existing state fields and DSML marker handling to locate the change.

Comment thread tests/unit/openai-bridge-upstream-responses.test.mjs
@awsl233777

Copy link
Copy Markdown
Collaborator Author

Updated the Codex streaming bridge to match dedicated Codex Responses→Chat proxy behavior instead of guessing via upstream /responses passthrough.

Changes:

  • Route streaming Codex /v1/responses requests directly through /chat/completions conversion.
  • Let the local bridge own the terminal SSE contract: always emit response.completed + [DONE] for converted chat streams.
  • Convert upstream retry exhaustion / stream errors into completed error text instead of response.failed, avoiding Codex CLI reconnect loops.
  • Updated regression tests so streaming Codex requests must not hit upstream /responses, and must not emit response.failed for 403, idle stream, or early-close cases.

Validation run locally:

  • node --test tests/unit/openai-bridge-upstream-responses.test.mjs — 30/30 pass
  • node --test tests/unit/openai-bridge-upstream-responses.test.mjs tests/unit/builtin-proxy-codex-responses.test.mjs — pass
  • npm run test:e2e — pass
  • npm run lint — pass
  • npm run test:unit — All 716 tests passed
  • git diff --check — pass
  • Real Codex CLI smoke test against this branch runtime: codex exec --full-auto --skip-git-repo-check "Reply with exactly OK" returned OK; log grep found no Reconnecting, no response.failed, and no stream closed before response.completed.

@coderabbitai re-review

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

@awsl233777 Thanks for the detailed update and validation, including the real Codex CLI smoke test. I’ll re-review the latest bridge changes now, with focus on the direct /chat/completions conversion path, the terminal SSE contract, and the no-response.failed Codex streaming behavior.


✏️ Learnings added
Learnt from: awsl233777
URL: https://github.com/SakuraByteCore/codexmate/pull/207

Timestamp: 2026-06-27T12:31:02.827Z
Learning: In `cli/openai-bridge.js`, streaming Codex `POST /v1/responses` requests are expected to bypass upstream `/v1/responses` passthrough and use local `/chat/completions` → Responses conversion instead; for these converted chat streams, the bridge should own the terminal SSE contract by emitting `response.completed` plus `data: [DONE]`, and should surface early upstream failure/retry exhaustion as completed error text rather than `response.failed` to avoid Codex CLI reconnect loops.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Action performed

Review finished.

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cli/openai-bridge.js (1)

2395-2408: 🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Stop retrying once SSE output has started.

streamChatCompletionsAsResponsesSse writes response.created before the upstream stream can time out, but the timeout handler still returns { ok: false, error: 'timeout' }. That makes retryTransientRequest re-run the stream on the same res after the response has already been committed. Guard the timeout path with a started flag and finalize the SSE stream instead of retrying once output has begun.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/openai-bridge.js` around lines 2395 - 2408, The retry loop around
retryTransientRequest in the streamChatCompletionsAsResponsesSse call is
re-running after SSE output has already started, which can write to the same res
twice. Add a started/output-begun guard in streamChatCompletionsAsResponsesSse
so the timeout path returns a non-retryable finalization once response.created
or any SSE data has been sent, and only allow { ok: false, error: 'timeout' }
before streaming begins. Use the existing streamChatCompletionsAsResponsesSse
and retryTransientRequest flow to stop retries after the first committed chunk
and finalize the SSE stream instead.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@cli/openai-bridge.js`:
- Around line 2395-2408: The retry loop around retryTransientRequest in the
streamChatCompletionsAsResponsesSse call is re-running after SSE output has
already started, which can write to the same res twice. Add a
started/output-begun guard in streamChatCompletionsAsResponsesSse so the timeout
path returns a non-retryable finalization once response.created or any SSE data
has been sent, and only allow { ok: false, error: 'timeout' } before streaming
begins. Use the existing streamChatCompletionsAsResponsesSse and
retryTransientRequest flow to stop retries after the first committed chunk and
finalize the SSE stream instead.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 167615e1-e04e-470c-9643-c984a3f64146

📥 Commits

Reviewing files that changed from the base of the PR and between 6b5dcc2 and 54d4b81.

📒 Files selected for processing (2)
  • cli/openai-bridge.js
  • tests/unit/openai-bridge-upstream-responses.test.mjs
📜 Review details
🔇 Additional comments (6)
tests/unit/openai-bridge-upstream-responses.test.mjs (3)

177-179: await bridge.close() / await upstream.close() don't await socket drain (http.Server.close is callback-based, not a promise), so this teardown can leave open handles and make the suite flaky. This is the same issue previously raised, and it now recurs in the new tests (Lines 177-179, 308-311, 439-442).


182-367: LGTM!


376-753: LGTM!

cli/openai-bridge.js (3)

1727-1734: LGTM!


2382-2394: LGTM!


1134-1163: 🩺 Stability & Availability

No missing SSE terminator here. sendResponsesSse already emits data: [DONE] after response.completed, so this error path still closes the stream correctly.

			> Likely an incorrect or invalid review comment.

@awsl233777

Copy link
Copy Markdown
Collaborator Author

Addressed the latest CodeRabbit feedback in ec07f51.

Fixes:

  • Added a committed-output guard in streamChatCompletionsAsResponsesSse.
    • Before any SSE output is committed, timeout/error may still return ok: false so the existing bounded retry flow can retry safely.
    • After downstream SSE has started (response.created / headers committed), timeout/error now finalizes the current SSE stream as completed error text and returns ok: true, preventing replay on the same res.
  • Converted the openai-bridge-upstream-responses test server teardown to promise-based closeServer(...) so cleanup waits for http.Server.close callbacks.
  • Tightened the idle accepted-chat-SSE test to assert the upstream chat request is made exactly once, proving no retry occurs after committed SSE output.

Validation run locally:

  • node --check cli/openai-bridge.js
  • node --check tests/unit/openai-bridge-upstream-responses.test.mjs
  • node --test tests/unit/openai-bridge-upstream-responses.test.mjs — 30/30 pass
  • node --test tests/unit/openai-bridge-upstream-responses.test.mjs tests/unit/builtin-proxy-codex-responses.test.mjs — pass
  • npm run test:e2e — pass
  • npm run lint — pass
  • npm run test:unit — All 716 tests passed
  • git diff --check — pass
  • Real Codex CLI smoke test against this branch runtime: codex exec --full-auto --skip-git-repo-check "Reply with exactly OK" returned OK; log grep found no Reconnecting, no response.failed, and no stream closed before response.completed.

@coderabbitai re-review

@coderabbitai

coderabbitai Bot commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

@awsl233777 Thanks for the update and the detailed validation. I’ll re-review ec07f51 with focus on the committed-output retry guard in streamChatCompletionsAsResponsesSse, the promise-based server teardown, and the “exactly once” idle SSE assertion.

(•ᴗ•)🐇


✏️ Learnings added
Learnt from: awsl233777
URL: https://github.com/SakuraByteCore/codexmate/pull/207

Timestamp: 2026-06-27T12:44:34.669Z
Learning: In `cli/openai-bridge.js`, `streamChatCompletionsAsResponsesSse` should only allow bounded retry before any downstream Responses SSE output is committed. After headers or `response.created` are committed, timeout/error paths should finalize the current SSE stream as completed error text and return success for that response flow, preventing a retry or replay on the same `res`.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

✅ Action performed

Review finished.

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

@awsl233777 awsl233777 force-pushed the fix/builtin-proxy-sub2api-hot-path branch from ec07f51 to a9c9fd6 Compare July 11, 2026 07:09
@awsl233777 awsl233777 changed the title fix(proxy): route builtin responses through chat hot path feat(providers): configure codex bridge retries Jul 11, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (4)
web-ui/partials/index/modals-basic.html (1)

56-67: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider adding max="10" to the retry input for UI consistency with the clamping range.

normalizeBridgeMaxRetries clamps to a maximum of 10, but the input only specifies min="2". A user entering 15 will see it silently clamped to 10 on blur with no visual hint of the upper bound. Adding max="10" aligns the HTML constraint with the normalization logic and improves discoverability.

♻️ Proposed addition
                     v-model.number="newProvider.openaiBridgeMaxRetries"
                     type="number"
                     min="2"
+                    max="10"
                     step="1"

Also applies to: 106-117

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/partials/index/modals-basic.html` around lines 56 - 67, Add max="10"
to the retry number inputs bound to newProvider.openaiBridgeMaxRetries,
including both the add and edit provider form sections, so their HTML
constraints match normalizeBridgeMaxRetries clamping.
web-ui/modules/app.methods.providers.mjs (1)

44-49: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

normalizeBridgeMaxRetries is duplicated in cli/openai-bridge.js.

The identical function appears at cli/openai-bridge.js:44-49. If this is an intentional UI/CLI boundary separation, consider adding a shared utility module to prevent the implementations from diverging over time. If the duplication is acceptable for now, a brief comment noting the mirror would help future maintainers.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/modules/app.methods.providers.mjs` around lines 44 - 49,
normalizeBridgeMaxRetries is duplicated between the UI module and
cli/openai-bridge.js, risking divergent behavior. Prefer extracting it into a
shared utility and update both callers to import it; if the boundary requires
duplication, add a brief comment in both implementations identifying the
mirrored function.
tests/unit/providers-validation.test.mjs (1)

183-213: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Good test for minimum clamping — consider adding a parallel test for maximum clamping.

The test correctly verifies that openaiBridgeMaxRetries: 1 normalizes to 2 in the payload. A companion test verifying that a value like 15 clamps to 10 would round out the boundary coverage and guard against future changes to the upper bound.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/providers-validation.test.mjs` around lines 183 - 213, Add a
companion test alongside the existing minimum-clamping test for the transform
provider flow, using openaiBridgeMaxRetries: 15 and asserting that
context.addProvider() submits openaiBridgeMaxRetries: 10 in the add-provider
payload. Reuse the existing createContext and apiCalls assertion pattern to
verify the upper-bound normalization.
cli.js (1)

2565-2584: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Duplicate retry-clamping logic; reuse the exported helper instead.

normalizeOpenaiBridgeMaxRetries reimplements the exact same clamp (default 2, range 2–10) as normalizeBridgeMaxRetries already exported from cli/openai-bridge.js (see DEFAULT_BRIDGE_MAX_RETRIES/MIN_BRIDGE_MAX_RETRIES/MAX_BRIDGE_MAX_RETRIES). Two independently maintained copies of this clamp risk silently drifting apart if the range/default is tuned in one place later.

♻️ Proposed fix
-function normalizeOpenaiBridgeMaxRetries(value, fallback = 2) {
-    const raw = Number(value);
-    const fallbackRaw = Number(fallback);
-    const base = Number.isFinite(raw) ? raw : (Number.isFinite(fallbackRaw) ? fallbackRaw : 2);
-    return Math.min(10, Math.max(2, Math.floor(base)));
-}
+// Reuse the canonical clamp from cli/openai-bridge.js instead of duplicating it here.
+const normalizeOpenaiBridgeMaxRetries = normalizeBridgeMaxRetries;

Requires normalizeBridgeMaxRetries to already be imported/required from ./cli/openai-bridge at the top of the file (it is exported there).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli.js` around lines 2565 - 2584, Replace the duplicate clamp implementation
in normalizeOpenaiBridgeMaxRetries with the imported normalizeBridgeMaxRetries
helper from ./cli/openai-bridge, preserving the existing fallback behavior and
provider resolution in resolveProviderOpenaiBridgeMaxRetries. Ensure the helper
is required or imported at the top of cli.js and remove the local normalization
logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cli.js`:
- Around line 12882-12885: Use the normalized retry count returned by
resolveOpenaiBridgeUpstream in both openai-bridge-get-provider and
buildMcpProviderListPayload. Replace
resolveProviderOpenaiBridgeMaxRetries(provider) and the related config.toml
lookup with upstream.maxRetries when constructing each response, while
preserving the existing baseUrl and hasApiKey fields.
- Around line 16350-16399: Update runOpenAiChatTaskNode so its
postOpenAiChatCompletion call is wrapped with retryTransientRequest. For
OpenAI-bridge providers, pass resolveProviderOpenaiBridgeMaxRetries(provider) as
the retry limit; for other providers, use a sane default fallback, preserving
the existing request arguments and error handling.

In `@web-ui/modules/app.methods.providers.mjs`:
- Around line 127-129: The validation in the provider validation function for
openaiBridgeMaxRetries is unreachable because normalizeBridgeMaxRetries already
clamps values to at least 2. Remove the openaiBridgeMaxRetries < 2 error
assignment and its corresponding term in the ok computation; alternatively,
validate the raw draft value before calling normalizeBridgeMaxRetries if
user-facing errors are required.

---

Nitpick comments:
In `@cli.js`:
- Around line 2565-2584: Replace the duplicate clamp implementation in
normalizeOpenaiBridgeMaxRetries with the imported normalizeBridgeMaxRetries
helper from ./cli/openai-bridge, preserving the existing fallback behavior and
provider resolution in resolveProviderOpenaiBridgeMaxRetries. Ensure the helper
is required or imported at the top of cli.js and remove the local normalization
logic.

In `@tests/unit/providers-validation.test.mjs`:
- Around line 183-213: Add a companion test alongside the existing
minimum-clamping test for the transform provider flow, using
openaiBridgeMaxRetries: 15 and asserting that context.addProvider() submits
openaiBridgeMaxRetries: 10 in the add-provider payload. Reuse the existing
createContext and apiCalls assertion pattern to verify the upper-bound
normalization.

In `@web-ui/modules/app.methods.providers.mjs`:
- Around line 44-49: normalizeBridgeMaxRetries is duplicated between the UI
module and cli/openai-bridge.js, risking divergent behavior. Prefer extracting
it into a shared utility and update both callers to import it; if the boundary
requires duplication, add a brief comment in both implementations identifying
the mirrored function.

In `@web-ui/partials/index/modals-basic.html`:
- Around line 56-67: Add max="10" to the retry number inputs bound to
newProvider.openaiBridgeMaxRetries, including both the add and edit provider
form sections, so their HTML constraints match normalizeBridgeMaxRetries
clamping.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c1b206dc-3349-4b8f-b960-bd020b246cc4

📥 Commits

Reviewing files that changed from the base of the PR and between 54d4b81 and a9c9fd6.

📒 Files selected for processing (16)
  • cli.js
  • cli/openai-bridge.js
  • tests/unit/openai-bridge-upstream-responses.test.mjs
  • tests/unit/provider-default-names.test.mjs
  • tests/unit/provider-share-command.test.mjs
  • tests/unit/provider-switch-regression.test.mjs
  • tests/unit/providers-validation.test.mjs
  • web-ui/app.js
  • web-ui/modules/app.methods.providers.mjs
  • web-ui/modules/i18n/locales/en.mjs
  • web-ui/modules/i18n/locales/ja.mjs
  • web-ui/modules/i18n/locales/vi.mjs
  • web-ui/modules/i18n/locales/zh-tw.mjs
  • web-ui/modules/i18n/locales/zh.mjs
  • web-ui/partials/index/modals-basic.html
  • web-ui/res/web-ui-render.precompiled.js
✅ Files skipped from review due to trivial changes (8)
  • web-ui/modules/i18n/locales/ja.mjs
  • web-ui/modules/i18n/locales/zh-tw.mjs
  • tests/unit/provider-switch-regression.test.mjs
  • web-ui/modules/i18n/locales/vi.mjs
  • web-ui/app.js
  • web-ui/modules/i18n/locales/en.mjs
  • web-ui/modules/i18n/locales/zh.mjs
  • tests/unit/provider-share-command.test.mjs
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: CodeRabbit
🧰 Additional context used
🪛 ast-grep (0.44.1)
cli.js

[warning] 10040-10040: Detects non-literal values in regular expressions
Context: new RegExp(^(\\s*${escapedFieldName}\\s*=\\s*)([-+]?\\d+(?:\\.\\d+)?)(\\s+#.*)?$, 'mg')
Note: [CWE-1333] Inefficient Regular Expression Complexity (ReDoS via non-literal RegExp).

(detect-non-literal-regexp)


[warning] 16660-16660: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.writeFileSync(normalized.path, fileContent, { encoding: 'utf-8', mode: 0o600 })
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(detect-non-literal-fs-filename)

🪛 OpenGrep (1.23.0)
cli.js

[ERROR] 16621-16621: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.

(coderabbit.command-injection.exec-js)

🔇 Additional comments (12)
web-ui/res/web-ui-render.precompiled.js (1)

7098-7127: LGTM!

Also applies to: 7240-7269

tests/unit/provider-default-names.test.mjs (1)

41-42: LGTM!

tests/unit/providers-validation.test.mjs (1)

25-26: LGTM!

Also applies to: 97-97

web-ui/partials/index/modals-basic.html (1)

56-67: 🎯 Functional Correctness

i18n keys already exist in all locales field.transformMaxRetries and hint.transformMaxRetries are present in every locale file.

			> Likely an incorrect or invalid review comment.
web-ui/modules/app.methods.providers.mjs (1)

450-456: 🎯 Functional Correctness

Remove the transform-off concern — the edit modal only shows bridge settings for existing transform providers, and the update path keeps those providers in the transform branch unless their stored config is no longer bridge-shaped. There’s no edit flow here that disables useTransform.

			> Likely an incorrect or invalid review comment.
tests/unit/openai-bridge-upstream-responses.test.mjs (2)

138-140: Unwrapped server.close() reintroduced in this new test.

Same issue flagged (and marked addressed) on other tests in this file: http.Server.close() without a callback doesn't return a promise, so await bridge.close() / await upstream.close() resolve immediately instead of waiting for sockets to drain, risking leaked handles/flakiness in this newly added test.

Proposed fix
-    await bridge.close();
-    await upstream.close();
+    await Promise.all([
+        new Promise((resolve, reject) => bridge.close((err) => err ? reject(err) : resolve())),
+        new Promise((resolve, reject) => upstream.close((err) => err ? reject(err) : resolve()))
+    ]);

96-141: LGTM on the retry-count assertion logic itself — maxRetries: 3 correctly yields 4 total upstream hits (attempt 0..3) and a final 502, matching retryTransientRequest's loop bounds.

cli.js (2)

2586-2738: LGTM! Wiring of openaiBridgeMaxRetries through addProviderToConfig, updateProviderInConfig, and cmdUpdate (including the new replaceTomlNumberField helper) is consistent — TOML and the bridge settings file are updated together, and the "no URL and no existing upstream" edge case correctly throws before any partial write.

Also applies to: 9873-10036, 10089-10170


10037-10062: LGTM! replaceTomlNumberField mirrors the existing replaceTomlStringField/replaceTomlBooleanField patterns (multiline-string range protection, fallback insertion). The static-analysis "non-literal RegExp" hint is a false positive here since fieldName is always the hardcoded literal 'codexmate_bridge_max_retries' from the sole call site and is passed through escapeRegex regardless.

cli/openai-bridge.js (3)

91-117: LGTM! normalizeBridgeMaxRetries, normalizeUpstreamEntry, and the updated upsertOpenaiBridgeProvider correctly derive, clamp, and persist maxRetries alongside the existing baseUrl/apiKey/headers handling.

Also applies to: 162-195


2211-2211: LGTM! All five upstream call sites now consistently pass { maxRetries: upstream.maxRetries }, and the export of normalizeBridgeMaxRetries is straightforward.

Also applies to: 2266-2266, 2313-2313, 2344-2344, 2406-2406, 2478-2478


1259-1286: 🩺 Stability & Availability

No change needed for result.retry
result.retry intentionally bubbles out so the caller can switch to the fallback URL/path; it is not meant to trigger another pass through retryTransientRequest.

			> Likely an incorrect or invalid review comment.

@awsl233777 awsl233777 force-pushed the fix/builtin-proxy-sub2api-hot-path branch from a9c9fd6 to 003a8ea Compare July 12, 2026 09:00
@awsl233777 awsl233777 force-pushed the fix/builtin-proxy-sub2api-hot-path branch from 003a8ea to 7628878 Compare July 12, 2026 09:05

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/unit/openai-bridge-upstream-responses.test.mjs (1)

1646-1660: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test exercises the writableEnded early-return, not the headersSent-but-not-ended branch it's named after.

patchedEnd calls originalEnd(chunk, encoding, cb) before throwing, so res.writableEnded is already true by the time the simulated error reaches the bridge's catch block. Per the referenced catch logic (if (res.writableEnded || res.destroyed) { return; } runs before the res.headersSent check), this test validates the writableEnded guard, not the "headers sent but response not yet ended" scenario the test name and layer description ("ends responses with sent headers") imply — e.g. a mid-stream failure after writeHead/write() but before end(). Consider throwing from a patched res.write() (exercised by the SSE/streaming path) instead of res.end() to actually cover the headersSent && !writableEnded fallback.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/openai-bridge-upstream-responses.test.mjs` around lines 1646 -
1660, Update the test’s response-failure simulation around patchedEnd so it
exercises the headersSent-but-not-writableEnded fallback. Patch res.write in the
SSE/streaming path to throw after writeHead or an initial write, before res.end
is called, and retain the assertion that no second writeHead occurs; avoid
calling originalEnd before the simulated failure.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/unit/openai-bridge-upstream-responses.test.mjs`:
- Around line 1638-1681: Wrap the listener registration, bridge request, and
assertions in a try/finally block within the test, and move
process.removeListener('uncaughtException', onUncaught) into the finally clause.
Ensure the existing server and temporary-directory cleanup also remains in
guaranteed cleanup paths so assertion failures cannot leak listeners or
resources.

---

Nitpick comments:
In `@tests/unit/openai-bridge-upstream-responses.test.mjs`:
- Around line 1646-1660: Update the test’s response-failure simulation around
patchedEnd so it exercises the headersSent-but-not-writableEnded fallback. Patch
res.write in the SSE/streaming path to throw after writeHead or an initial
write, before res.end is called, and retain the assertion that no second
writeHead occurs; avoid calling originalEnd before the simulated failure.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: b80010b4-42e8-4371-b4fe-c4a9d3f671ef

📥 Commits

Reviewing files that changed from the base of the PR and between a9c9fd6 and 003a8ea.

📒 Files selected for processing (2)
  • cli/openai-bridge.js
  • tests/unit/openai-bridge-upstream-responses.test.mjs
🚧 Files skipped from review as they are similar to previous changes (1)
  • cli/openai-bridge.js
📜 Review details
⏰ Context from checks skipped due to timeout. (3)
  • GitHub Check: CodeRabbit
  • GitHub Check: ci (20)
  • GitHub Check: ci (18)
🔇 Additional comments (1)
tests/unit/openai-bridge-upstream-responses.test.mjs (1)

1613-1685: 🩺 Stability & Availability

Likely repeats the unwrapped server.close() pattern flagged earlier in this file.

Per file convention (see lines 138-140, 224-226, 291-293, 1608-1610), cleanup for this new test almost certainly ends with raw await bridge.close(); await upstream.close(); calls, which don't actually wait for socket drain since http.Server.close() isn't a promise. This exact concern was raised previously on this file and marked "Addressed in commit ec07f51," yet the same unwrapped pattern still appears at multiple points in the file shown here, so it's worth confirming whether the new test's (unseen) teardown at lines ~1682-1685 was updated to use promise-wrapped shutdown.

#!/bin/bash
rg -n 'close\(' tests/unit/openai-bridge-upstream-responses.test.mjs | tail -20
sed -n '1680,1690p' tests/unit/openai-bridge-upstream-responses.test.mjs

Comment on lines +1638 to +1681
let uncaught = null;
const onUncaught = (err) => { uncaught = err; };
process.once('uncaughtException', onUncaught);

const handler = createOpenaiBridgeHttpHandler({ settingsFile, expectedToken: 'codexmate' });
const bridge = http.createServer((req, res) => {
const originalWriteHead = res.writeHead.bind(res);
let wroteSuccessHeader = false;
res.writeHead = function patchedWriteHead(statusCode, headers) {
if (statusCode === 200) wroteSuccessHeader = true;
if (wroteSuccessHeader && statusCode === 500) {
throw new Error('regression: attempted second writeHead after headers sent');
}
return originalWriteHead(statusCode, headers);
};
const originalEnd = res.end.bind(res);
res.end = function patchedEnd(chunk, encoding, cb) {
if (wroteSuccessHeader && !res.writableEnded) {
originalEnd(chunk, encoding, cb);
throw new Error('simulated post-header write failure');
}
return originalEnd(chunk, encoding, cb);
};
if (!handler(req, res)) {
res.statusCode = 404;
res.end('not handled');
}
});
const { port: bridgePort } = await listen(bridge);

const resp = await requestText(`http://127.0.0.1:${bridgePort}/bridge/openai/test/v1/responses`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer codexmate'
},
body: { model: 'gpt-test', input: 'ping' }
});

assert.equal(resp.status, 200);
assert.equal(uncaught, null);

process.removeListener('uncaughtException', onUncaught);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Wrap cleanup in try/finally to avoid leaking the uncaughtException listener on assertion failure.

If assert.equal(resp.status, 200) or assert.equal(uncaught, null) fails, process.removeListener('uncaughtException', onUncaught) (and presumably the subsequent server/tmpDir cleanup) never runs. Since registering any 'uncaughtException' listener suppresses Node's default crash behavior, a leaked .once() listener can silently consume an unrelated uncaught exception thrown by a later test in the same process, masking a real failure.

Proposed fix
-    const resp = await requestText(`http://127.0.0.1:${bridgePort}/bridge/openai/test/v1/responses`, {
-        method: 'POST',
-        headers: {
-            'Content-Type': 'application/json',
-            'Accept': 'application/json',
-            'Authorization': 'Bearer codexmate'
-        },
-        body: { model: 'gpt-test', input: 'ping' }
-    });
-
-    assert.equal(resp.status, 200);
-    assert.equal(uncaught, null);
-
-    process.removeListener('uncaughtException', onUncaught);
+    try {
+        const resp = await requestText(`http://127.0.0.1:${bridgePort}/bridge/openai/test/v1/responses`, {
+            method: 'POST',
+            headers: {
+                'Content-Type': 'application/json',
+                'Accept': 'application/json',
+                'Authorization': 'Bearer codexmate'
+            },
+            body: { model: 'gpt-test', input: 'ping' }
+        });
+
+        assert.equal(resp.status, 200);
+        assert.equal(uncaught, null);
+    } finally {
+        process.removeListener('uncaughtException', onUncaught);
+    }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
let uncaught = null;
const onUncaught = (err) => { uncaught = err; };
process.once('uncaughtException', onUncaught);
const handler = createOpenaiBridgeHttpHandler({ settingsFile, expectedToken: 'codexmate' });
const bridge = http.createServer((req, res) => {
const originalWriteHead = res.writeHead.bind(res);
let wroteSuccessHeader = false;
res.writeHead = function patchedWriteHead(statusCode, headers) {
if (statusCode === 200) wroteSuccessHeader = true;
if (wroteSuccessHeader && statusCode === 500) {
throw new Error('regression: attempted second writeHead after headers sent');
}
return originalWriteHead(statusCode, headers);
};
const originalEnd = res.end.bind(res);
res.end = function patchedEnd(chunk, encoding, cb) {
if (wroteSuccessHeader && !res.writableEnded) {
originalEnd(chunk, encoding, cb);
throw new Error('simulated post-header write failure');
}
return originalEnd(chunk, encoding, cb);
};
if (!handler(req, res)) {
res.statusCode = 404;
res.end('not handled');
}
});
const { port: bridgePort } = await listen(bridge);
const resp = await requestText(`http://127.0.0.1:${bridgePort}/bridge/openai/test/v1/responses`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer codexmate'
},
body: { model: 'gpt-test', input: 'ping' }
});
assert.equal(resp.status, 200);
assert.equal(uncaught, null);
process.removeListener('uncaughtException', onUncaught);
let uncaught = null;
const onUncaught = (err) => { uncaught = err; };
process.once('uncaughtException', onUncaught);
const handler = createOpenaiBridgeHttpHandler({ settingsFile, expectedToken: 'codexmate' });
const bridge = http.createServer((req, res) => {
const originalWriteHead = res.writeHead.bind(res);
let wroteSuccessHeader = false;
res.writeHead = function patchedWriteHead(statusCode, headers) {
if (statusCode === 200) wroteSuccessHeader = true;
if (wroteSuccessHeader && statusCode === 500) {
throw new Error('regression: attempted second writeHead after headers sent');
}
return originalWriteHead(statusCode, headers);
};
const originalEnd = res.end.bind(res);
res.end = function patchedEnd(chunk, encoding, cb) {
if (wroteSuccessHeader && !res.writableEnded) {
originalEnd(chunk, encoding, cb);
throw new Error('simulated post-header write failure');
}
return originalEnd(chunk, encoding, cb);
};
if (!handler(req, res)) {
res.statusCode = 404;
res.end('not handled');
}
});
const { port: bridgePort } = await listen(bridge);
try {
const resp = await requestText(`http://127.0.0.1:${bridgePort}/bridge/openai/test/v1/responses`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
'Authorization': 'Bearer codexmate'
},
body: { model: 'gpt-test', input: 'ping' }
});
assert.equal(resp.status, 200);
assert.equal(uncaught, null);
} finally {
process.removeListener('uncaughtException', onUncaught);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/openai-bridge-upstream-responses.test.mjs` around lines 1638 -
1681, Wrap the listener registration, bridge request, and assertions in a
try/finally block within the test, and move
process.removeListener('uncaughtException', onUncaught) into the finally clause.
Ensure the existing server and temporary-directory cleanup also remains in
guaranteed cleanup paths so assertion failures cannot leak listeners or
resources.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
cli/openai-bridge-retry.js (1)

30-33: 🚀 Performance & Scalability | 🔵 Trivial

Consider respecting Retry-After for 429/503 if available.

getTransientRetryDelayMs always uses fixed exponential backoff regardless of status. For 429/503 many upstreams return a Retry-After header indicating the correct wait time; a blind exponential backoff can under- or over-wait relative to what the provider expects. This is only actionable if result (from proxyRequestJson) surfaces response headers to retryTransientRequest — worth checking before implementing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/openai-bridge-retry.js` around lines 30 - 33, Update the transient retry
flow around getTransientRetryDelayMs and retryTransientRequest to inspect
response headers from proxyRequestJson when available; for 429/503 responses,
parse a valid Retry-After value and use it as the retry delay, while preserving
the existing capped exponential backoff when headers are absent or invalid. If
proxyRequestJson does not expose headers, first extend its result handling so
retryTransientRequest can access them.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@cli/openai-bridge-retry.js`:
- Around line 30-33: Update the transient retry flow around
getTransientRetryDelayMs and retryTransientRequest to inspect response headers
from proxyRequestJson when available; for 429/503 responses, parse a valid
Retry-After value and use it as the retry delay, while preserving the existing
capped exponential backoff when headers are absent or invalid. If
proxyRequestJson does not expose headers, first extend its result handling so
retryTransientRequest can access them.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: cfe554ec-c3f6-4d8b-854e-3c9ca4d9349c

📥 Commits

Reviewing files that changed from the base of the PR and between 7628878 and 563d3bf.

📒 Files selected for processing (3)
  • cli/openai-bridge-retry.js
  • cli/openai-bridge.js
  • tests/unit/openai-bridge-upstream-responses.test.mjs
🚧 Files skipped from review as they are similar to previous changes (1)
  • cli/openai-bridge.js
📜 Review details
⏰ Context from checks skipped due to timeout. (3)
  • GitHub Check: CodeRabbit
  • GitHub Check: ci (18)
  • GitHub Check: ci (18)
🔇 Additional comments (5)
tests/unit/openai-bridge-upstream-responses.test.mjs (3)

144-197: Recurring un-promisified shutdown + no try/finally (same class of issue flagged previously in this file).

bridge.close()/upstream.close() at Lines 193-194 are raw http.Server.close() calls — await resolves immediately without waiting for socket drain, same as the earlier flagged/addressed instance. Additionally, none of the assertions (Lines 189-191) are wrapped in try/finally, so a failed assertion (e.g. hitCount !== 3) skips server/tmpDir cleanup entirely, leaking two listening servers and a temp directory into later test runs.

Proposed fix
-    const resp = await requestText(`http://127.0.0.1:${bridgePort}/bridge/openai/test/v1/models`, {
-        headers: { Authorization: 'Bearer codexmate' }
-    });
-
-    assert.equal(resp.status, 200);
-    assert.equal(hitCount, 3);
-    assert.deepEqual(JSON.parse(resp.text), { object: 'list', data: [{ id: 'gpt-test' }] });
-
-    await bridge.close();
-    await upstream.close();
-    await rm(tmpDir, { recursive: true, force: true });
+    try {
+        const resp = await requestText(`http://127.0.0.1:${bridgePort}/bridge/openai/test/v1/models`, {
+            headers: { Authorization: 'Bearer codexmate' }
+        });
+
+        assert.equal(resp.status, 200);
+        assert.equal(hitCount, 3);
+        assert.deepEqual(JSON.parse(resp.text), { object: 'list', data: [{ id: 'gpt-test' }] });
+    } finally {
+        await Promise.all([
+            new Promise((resolve, reject) => bridge.close((err) => err ? reject(err) : resolve())),
+            new Promise((resolve, reject) => upstream.close((err) => err ? reject(err) : resolve()))
+        ]);
+        await rm(tmpDir, { recursive: true, force: true });
+    }

1668-1740: 🩺 Stability & Availability

Verify the previously-flagged try/finally fix for the uncaughtException listener was applied here.

This range (per the line-range summary) is the "no HTTP 500/second write after headers sent" regression test, matching the test a prior review flagged for leaking the uncaughtException listener on assertion failure — that prior comment doesn't carry an "✅ Addressed" marker (unlike the sibling comment on the close()-promisification fix), so it may still be open. This exact code isn't in the provided snippet; please confirm current state.

#!/bin/bash
sed -n '1655,1745p' tests/unit/openai-bridge-upstream-responses.test.mjs

1742-1809: Same close()-promisification/try-finally gap as the earlier new test.

Lines 1806-1808 repeat the raw await bridge.close(); await upstream.close(); pattern without try/finally around the assertions above (Lines 1793-1804), so a failed assert.rejects/assert.equal(destroyCalled, ...) again leaks both servers and tmpDir. Same fix as proposed for the 524-retry test applies here.

Aside from that, the test logic itself is sound: since the patched res.end throws before invoking the original, no bytes are ever flushed to the socket (Node buffers headers until the first write/end), so the eventual res.destroy(e) manifests to the client as a connection-reset on the request object, which requestText's req.on('error', reject) correctly surfaces via assert.rejects.

cli/openai-bridge-retry.js (2)

14-17: 🎯 Functional Correctness

Confirm intent of treating 409/425 as transient/retryable.

Standard retry sets for HTTP APIs (OpenAI-compatible gateways, gRPC-derived retry policies, etc.) typically cover 408/429/500/502/503/504 (and sometimes 499); 409 Conflict and 425 Too Early are unusual additions — a 409 conflict generally won't resolve from a blind retry of the same request, and blindly retrying it can mask a real error or waste retry budget before surfacing a definitive failure to the user.

If a specific upstream provider is known to emit 409/425 transiently (e.g. as a busy/lock signal), this is fine — otherwise consider dropping them from the transient set.


51-56: LGTM!

Also applies to: 68-68

@awsl233777 awsl233777 changed the title feat(providers): configure codex bridge retries fix(openai-bridge): harden retries and response cleanup Jul 12, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web-ui/styles/layout-shell.css`:
- Around line 41-43: Insert a blank line between the --side-rail-width
custom-property declaration and the grid-template-columns declaration in
.app-shell.sidebar-collapsed to satisfy declaration-empty-line-before.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9debaad6-dcc7-4062-82d2-f9fd62fe5649

📥 Commits

Reviewing files that changed from the base of the PR and between 4372218 and 6431495.

📒 Files selected for processing (1)
  • web-ui/styles/layout-shell.css
📜 Review details
⏰ Context from checks skipped due to timeout. (3)
  • GitHub Check: CodeRabbit
  • GitHub Check: ci (18)
  • GitHub Check: ci (18)
🧰 Additional context used
🪛 Stylelint (17.14.0)
web-ui/styles/layout-shell.css

[error] 43-43: Expected empty line before declaration (declaration-empty-line-before)

(declaration-empty-line-before)

Comment on lines 41 to +43
.app-shell.sidebar-collapsed {
--side-rail-width: var(--side-rail-collapsed-width, 136px);
grid-template-columns: var(--side-rail-collapsed-width, 136px) minmax(0, 1fr);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add the required blank line before the new declaration.

Stylelint reports declaration-empty-line-before for line 43; insert an empty line after the custom-property declaration.

Proposed fix
 .app-shell.sidebar-collapsed {
     --side-rail-width: var(--side-rail-collapsed-width, 136px);
+
     grid-template-columns: var(--side-rail-collapsed-width, 136px) minmax(0, 1fr);
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
.app-shell.sidebar-collapsed {
--side-rail-width: var(--side-rail-collapsed-width, 136px);
grid-template-columns: var(--side-rail-collapsed-width, 136px) minmax(0, 1fr);
.app-shell.sidebar-collapsed {
--side-rail-width: var(--side-rail-collapsed-width, 136px);
grid-template-columns: var(--side-rail-collapsed-width, 136px) minmax(0, 1fr);
}
🧰 Tools
🪛 Stylelint (17.14.0)

[error] 43-43: Expected empty line before declaration (declaration-empty-line-before)

(declaration-empty-line-before)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/styles/layout-shell.css` around lines 41 - 43, Insert a blank line
between the --side-rail-width custom-property declaration and the
grid-template-columns declaration in .app-shell.sidebar-collapsed to satisfy
declaration-empty-line-before.

Source: Linters/SAST tools

@awsl233777 awsl233777 changed the title fix(openai-bridge): harden retries and response cleanup fix(openai-bridge): harden retries and collapsed layout Jul 12, 2026
@awsl233777 awsl233777 changed the title fix(openai-bridge): harden retries and collapsed layout fix(openai-bridge): hide retry counts and harden retries Jul 12, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cli/openai-bridge-retry.js (1)

14-17: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Honor Retry-After on transient retries. isTransientHttpStatus() already includes 429, but retryTransientRequest() always uses the fixed backoff and ignores the server’s Retry-After hint, so rate-limited requests can be retried too early.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/openai-bridge-retry.js` around lines 14 - 17, Update
retryTransientRequest() to read the response’s Retry-After header for transient
HTTP failures, including status 429, and delay the retry until at least the
server-specified duration. Preserve the existing fixed backoff as the fallback
when the header is absent or invalid, while leaving isTransientHttpStatus()
unchanged.
🧹 Nitpick comments (2)
cli/openai-bridge.js (1)

512-529: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Consider logging when res.end() succeeds after a mid-flight error.

When res.headersSent is true and res.end() succeeds (line 519), the original error e is silently swallowed — no log, no metric. This makes mid-flight failures invisible to operators. A single console.error or structured log here would aid debugging without changing behavior.

♻️ Suggested addition
             if (res.headersSent) {
                 try {
                     res.end();
                 } catch (_) {
                     // Headers are already committed. Close the socket instead of leaving the client waiting forever.
                     if (!res.destroyed && typeof res.destroy === 'function') {
                         res.destroy(e);
                     }
                 }
+                // Best-effort log: the client received a truncated response.
+                if (typeof console !== 'undefined' && console.error) {
+                    console.error('[openai-bridge] mid-flight error after headers sent:', message);
+                }
                 return;
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/openai-bridge.js` around lines 512 - 529, Add logging for the original
error in the res.headersSent branch of the catch handler when res.end()
completes successfully, while preserving the existing response behavior and
fallback socket-destruction handling.
cli/openai-bridge-retry.js (1)

38-58: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Consider a maximum total retry duration or attempt cap for production safety.

With DEFAULT_BRIDGE_MAX_RETRIES = Infinity and backoff capped at 5s, a persistently failing upstream will cause the bridge to retry indefinitely (~5s per attempt after ~8 attempts). Multiple concurrent clients hitting a failing upstream could accumulate retry loops holding connections open, risking resource exhaustion. Consider adding an optional maxRetryDurationMs or a high finite ceiling (e.g., 100) as a safety net, even if the default remains effectively unlimited.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/openai-bridge-retry.js` around lines 38 - 58, Add a production safety
bound to the retry loop in the bridge executor, using either an optional
maxRetryDurationMs or a high finite attempt cap while preserving the existing
retry behavior by default. Update the loop around executor(attempt) to stop
retrying once the configured duration or cap is reached, and return the latest
result rather than continuing indefinitely.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@cli/openai-bridge-retry.js`:
- Around line 14-17: Update retryTransientRequest() to read the response’s
Retry-After header for transient HTTP failures, including status 429, and delay
the retry until at least the server-specified duration. Preserve the existing
fixed backoff as the fallback when the header is absent or invalid, while
leaving isTransientHttpStatus() unchanged.

---

Nitpick comments:
In `@cli/openai-bridge-retry.js`:
- Around line 38-58: Add a production safety bound to the retry loop in the
bridge executor, using either an optional maxRetryDurationMs or a high finite
attempt cap while preserving the existing retry behavior by default. Update the
loop around executor(attempt) to stop retrying once the configured duration or
cap is reached, and return the latest result rather than continuing
indefinitely.

In `@cli/openai-bridge.js`:
- Around line 512-529: Add logging for the original error in the res.headersSent
branch of the catch handler when res.end() completes successfully, while
preserving the existing response behavior and fallback socket-destruction
handling.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e0e3cc67-4423-439f-beb5-c405c514dc89

📥 Commits

Reviewing files that changed from the base of the PR and between 6431495 and 187a747.

📒 Files selected for processing (15)
  • cli/openai-bridge-retry.js
  • cli/openai-bridge.js
  • tests/unit/openai-bridge-upstream-responses.test.mjs
  • tests/unit/provider-default-names.test.mjs
  • tests/unit/provider-switch-regression.test.mjs
  • tests/unit/providers-validation.test.mjs
  • web-ui/app.js
  • web-ui/modules/app.methods.providers.mjs
  • web-ui/modules/i18n/locales/en.mjs
  • web-ui/modules/i18n/locales/ja.mjs
  • web-ui/modules/i18n/locales/vi.mjs
  • web-ui/modules/i18n/locales/zh-tw.mjs
  • web-ui/modules/i18n/locales/zh.mjs
  • web-ui/partials/index/modals-basic.html
  • web-ui/res/web-ui-render.precompiled.js
💤 Files with no reviewable changes (8)
  • web-ui/modules/i18n/locales/zh-tw.mjs
  • web-ui/modules/i18n/locales/zh.mjs
  • web-ui/modules/i18n/locales/en.mjs
  • web-ui/modules/i18n/locales/ja.mjs
  • web-ui/partials/index/modals-basic.html
  • web-ui/modules/i18n/locales/vi.mjs
  • web-ui/res/web-ui-render.precompiled.js
  • tests/unit/provider-default-names.test.mjs
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: CodeRabbit
🔇 Additional comments (23)
cli/openai-bridge.js (5)

434-445: 🩺 Stability & Availability

Streaming retry safety concern — see verification request on openai-bridge-retry.js.

This call passes res directly to streamChatCompletionsAsResponsesSse. If the streaming function commits headers/SSE output and then fails with a transient status, the retry loop (which checks result.status before result.retry) will retry and attempt to write to an already-committed response. The downstream error handling at lines 446-458 checks res.headersSent, but that only runs after retryTransientRequest returns — it doesn't prevent the retry itself.


108-119: LGTM!


165-195: LGTM!


374-383: LGTM!

Also applies to: 463-470


551-558: LGTM!

tests/unit/openai-bridge-upstream-responses.test.mjs (3)

1810-1877: 🩺 Stability & Availability

Verify whether the promise-wrapped server shutdown pattern is applied consistently in this new test.

A past review comment flagged that await bridge.close() / await upstream.close() return immediately because http.Server.close() returns the server object, not a Promise. The comment was marked "✅ Addressed in commit ec07f51," but this newly added test (lines 1874-1876) still uses the raw await pattern. If the fix was applied to other tests, this new test should use the same pattern for consistency and to avoid leaking open handles.

#!/bin/bash
# Check if any tests in this file use promise-wrapped server.close()
rg -n 'new Promise.*close' tests/unit/openai-bridge-upstream-responses.test.mjs
# Check the pattern used for server shutdown across all tests
rg -n 'await (bridge|upstream)\.close' tests/unit/openai-bridge-upstream-responses.test.mjs

96-146: LGTM!

Also applies to: 148-199, 202-259


1736-1808: LGTM!

cli/openai-bridge-retry.js (2)

1-11: LGTM!


38-58: 🩺 Stability & Availability

No retry-after-commit issue here. streamChatCompletionsAsResponsesSse() only returns { status: ... } before any SSE headers are written; once it has committed output, it resolves { ok: true } or finishes the stream with SSE failure events. The retry loop won’t retry a committed stream on the current code path.

			> Likely an incorrect or invalid review comment.
web-ui/app.js (1)

293-295: LGTM!

web-ui/modules/app.methods.providers.mjs (8)

44-58: LGTM!


72-124: LGTM!


173-223: LGTM!


318-344: LGTM!


346-414: LGTM!


416-466: LGTM!


473-473: LGTM!


556-556: LGTM!

tests/unit/providers-validation.test.mjs (3)

25-26: LGTM!


97-97: LGTM!


184-208: LGTM!

tests/unit/provider-switch-regression.test.mjs (1)

273-273: LGTM!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cli/openai-bridge-runtime.js`:
- Around line 1475-1488: Add the next sequence value to the top-level payload of
the response.created event, matching the Responses streaming schema. Keep
response.in_progress using its existing state.nextSeq() call and do not alter
its ordering behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 7119aa9e-aba4-4999-bb61-13c1741128ce

📥 Commits

Reviewing files that changed from the base of the PR and between 187a747 and 5c9c66d.

📒 Files selected for processing (3)
  • cli/openai-bridge-retry.js
  • cli/openai-bridge-runtime.js
  • tests/unit/openai-bridge-upstream-responses.test.mjs
🚧 Files skipped from review as they are similar to previous changes (2)
  • cli/openai-bridge-retry.js
  • tests/unit/openai-bridge-upstream-responses.test.mjs
📜 Review details
⏰ Context from checks skipped due to timeout. (1)
  • GitHub Check: CodeRabbit

Comment on lines +1475 to +1488
created_at: state.createdAt,
status: 'in_progress',
output: []
}
});
writeSse(res, 'response.in_progress', {
type: 'response.in_progress',
sequence_number: state.nextSeq(),
response: {
id: state.responseId,
model: state.model,
created_at: state.createdAt,
status: 'in_progress',
output: []

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -eu
sed -n '1468,1492p' cli/openai-bridge-runtime.js
rg -n -A8 -B4 'response\.created|response\.in_progress|sequence_number' tests/unit/openai-bridge-upstream-responses.test.mjs

Repository: SakuraByteCore/codexmate

Length of output: 997


🏁 Script executed:

#!/bin/bash
set -eu

printf '\n== response event emitters ==\n'
rg -n -A6 -B4 "writeSse\\(res, 'response\\.(created|in_progress|completed|failed|incomplete)'" cli/openai-bridge-runtime.js

printf '\n== sequence number helpers/usages ==\n'
rg -n "nextSeq\\(|sequence_number" cli tests --glob '!**/node_modules/**'

printf '\n== response streaming tests ==\n'
sed -n '1,260p' tests/unit/openai-bridge-upstream-responses.test.mjs

Repository: SakuraByteCore/codexmate

Length of output: 17661


🏁 Script executed:

#!/bin/bash
set -eu

printf '\n== shared Responses SSE emitter ==\n'
sed -n '800,920p' cli/openai-bridge-runtime.js

printf '\n== tests mentioning response.created or sequence_number ==\n'
rg -n -A4 -B4 "response\\.created|response\\.in_progress|sequence_number" tests cli/openai-bridge-runtime.js cli/openai-bridge.js

printf '\n== response SSE parsing helpers in tests ==\n'
rg -n -A5 -B5 "event: 'response\\.|type: 'response\\.|sequence_number" tests/unit/openai-bridge-upstream-responses.test.mjs

Repository: SakuraByteCore/codexmate

Length of output: 18992


🌐 Web query:

OpenAI Responses streaming schema response.created sequence_number required response.in_progress

💡 Result:

In the OpenAI Responses API, streaming events follow a structured schema that includes both type and sequence_number fields [1][2]. Key Event Types: - response.created: Emitted immediately after a response is created. It includes the response object and the sequence_number [2][3]. - response.in_progress: Emitted while the response is being generated. Like other lifecycle events, it includes the response object and the sequence_number [2][3]. Sequence Number: The sequence_number is a required integer field for streaming events in the Responses API [1][4][5]. It is monotonically increasing for each stream and is used by clients to enforce correct event ordering and to ignore duplicate events [6]. Documentation and Implementation: Official OpenAI documentation defines these events with a specific schema [1]. Client libraries, such as the official Node.js SDK, define these events in their type definitions (e.g., ResponseCreatedEvent and ResponseInProgressEvent) as requiring both a type and a sequence_number [7][2]. While the API specification mandates these fields, developers have noted that implementation behavior can occasionally deviate from the spec in specific scenarios (such as error events), requiring robust handling in client implementations [5]. Note: This structure specifically applies to the Responses API (often used in the context of the Assistants API/Realtime capabilities) and differs from the traditional Chat Completions streaming API, which utilizes a different chunk-based structure (chat.completion.chunk) [8][9][10].

Citations:


🏁 Script executed:

#!/bin/bash
set -eu

printf '\n== local docs for Responses sequencing ==\n'
rg -n "sequence_number|response\\.created|response\\.in_progress|Responses streaming|streaming schema" README* docs cli tests --glob '!**/node_modules/**'

printf '\n== all response.created emitters ==\n'
rg -n -A5 -B3 "writeSse\\(res, 'response\\.created'" cli tests

Repository: SakuraByteCore/codexmate

Length of output: 4152


Add sequence_number to response.created. This lifecycle event is missing the top-level ordering field, so it doesn’t match the Responses streaming schema; response.in_progress should keep the next sequence value.

Proposed fix
                     created_at: state.createdAt,
                     status: 'in_progress',
                     output: []
-                }
+                },
+                sequence_number: state.nextSeq()
             });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
created_at: state.createdAt,
status: 'in_progress',
output: []
}
});
writeSse(res, 'response.in_progress', {
type: 'response.in_progress',
sequence_number: state.nextSeq(),
response: {
id: state.responseId,
model: state.model,
created_at: state.createdAt,
status: 'in_progress',
output: []
created_at: state.createdAt,
status: 'in_progress',
output: []
},
sequence_number: state.nextSeq()
});
writeSse(res, 'response.in_progress', {
type: 'response.in_progress',
sequence_number: state.nextSeq(),
response: {
id: state.responseId,
model: state.model,
created_at: state.createdAt,
status: 'in_progress',
output: []
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/openai-bridge-runtime.js` around lines 1475 - 1488, Add the next sequence
value to the top-level payload of the response.created event, matching the
Responses streaming schema. Keep response.in_progress using its existing
state.nextSeq() call and do not alter its ordering behavior.

Source: MCP tools

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web-ui/modules/app.methods.openclaw-core.mjs`:
- Around line 500-538: Update getOpenclawConfigSummary’s legacy agent.model
fallback to handle both string and plain-object values, using the object’s
primary field when present before passing the result to
formatOpenclawSummaryValue. Preserve the existing agents.defaults model handling
and ensure object-form legacy configurations no longer render as [object
Object].
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 787ea684-4609-4c8c-89ff-7b53f7111667

📥 Commits

Reviewing files that changed from the base of the PR and between 5c9c66d and f1429fa.

📒 Files selected for processing (6)
  • tests/unit/openclaw-core.test.mjs
  • tests/unit/web-ui-behavior-parity.test.mjs
  • web-ui/modules/app.methods.openclaw-core.mjs
  • web-ui/partials/index/panel-config-openclaw.html
  • web-ui/res/web-ui-render.precompiled.js
  • web-ui/styles/openclaw-structured.css
📜 Review details
🔇 Additional comments (12)
web-ui/modules/app.methods.openclaw-core.mjs (3)

227-237: LGTM!


239-250: LGTM!


540-561: LGTM!

tests/unit/openclaw-core.test.mjs (1)

453-501: LGTM!

tests/unit/web-ui-behavior-parity.test.mjs (1)

835-839: LGTM!

web-ui/partials/index/panel-config-openclaw.html (2)

14-48: LGTM!


96-100: LGTM!

web-ui/res/web-ui-render.precompiled.js (2)

2246-2288: LGTM!


2359-2371: LGTM!

web-ui/styles/openclaw-structured.css (3)

366-496: LGTM!


674-690: LGTM!


711-719: LGTM!

Comment on lines +500 to +538
getOpenclawConfigSummary(config) {
const content = config && typeof config.content === 'string' ? config.content : '';
if (!content.trim()) {
return [];
}
const parsed = this.parseOpenclawContent(content, { allowEmpty: true });
if (!parsed.ok || !isPlainRecord(parsed.data)) {
return [
{ key: 'parse-error', label: '配置状态', value: '解析失败', tone: 'warning' }
];
}
const data = parsed.data;
const agentDefaults = readNestedValue(data, ['agents', 'defaults']);
const modelConfig = isPlainRecord(agentDefaults) ? agentDefaults.model : undefined;
const primaryModel = isPlainRecord(modelConfig)
? modelConfig.primary
: (typeof modelConfig === 'string' ? modelConfig : readNestedValue(data, ['agent', 'model']));
const fallbacks = isPlainRecord(modelConfig) && Array.isArray(modelConfig.fallbacks)
? modelConfig.fallbacks
: [];
const workspace = isPlainRecord(agentDefaults) ? agentDefaults.workspace : '';
const browser = isPlainRecord(data.browser) ? data.browser : {};
const browserMode = browser.enabled === false
? '关闭'
: (browser.headless === true ? 'Headless' : (browser.headless === false ? '可见窗口' : '未声明'));
const executablePath = typeof browser.executablePath === 'string' ? browser.executablePath.trim() : '';
const providerNames = collectDistinctProviderKeys(
readNestedValue(data, ['models', 'providers']),
data.providers
);
return [
{ key: 'primary', label: '默认模型', value: formatOpenclawSummaryValue(primaryModel) },
{ key: 'fallbacks', label: 'Fallback', value: formatOpenclawSummaryValue(fallbacks) },
{ key: 'workspace', label: 'Workspace', value: formatOpenclawSummaryValue(workspace) },
{ key: 'browser', label: 'Browser', value: browserMode },
{ key: 'browser-path', label: 'Chrome', value: formatOpenclawSummaryValue(executablePath) },
{ key: 'providers', label: 'Providers', value: providerNames.length ? providerNames.join(' / ') : '未配置' }
];
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Legacy agent.model object format produces [object Object] in summary.

When agents.defaults is absent and the config uses the legacy agent: { model: { primary: 'openai/gpt-4' } } format, line 516 falls back to readNestedValue(data, ['agent', 'model']) which returns the object directly. formatOpenclawSummaryValue then converts it to [object Object] via String(value). The existing fillOpenclawQuickFromConfig (lines 376-382) handles this case by checking for string vs object with .primary. The summary method should do the same.

🐛 Proposed fix for legacy model fallback
             const agentDefaults = readNestedValue(data, ['agents', 'defaults']);
             const modelConfig = isPlainRecord(agentDefaults) ? agentDefaults.model : undefined;
+            const legacyModel = readNestedValue(data, ['agent', 'model']);
+            const legacyPrimary = typeof legacyModel === 'string'
+                ? legacyModel
+                : (isPlainRecord(legacyModel) && typeof legacyModel.primary === 'string' ? legacyModel.primary : undefined);
             const primaryModel = isPlainRecord(modelConfig)
                 ? modelConfig.primary
-                : (typeof modelConfig === 'string' ? modelConfig : readNestedValue(data, ['agent', 'model']));
+                : (typeof modelConfig === 'string' ? modelConfig : legacyPrimary);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
getOpenclawConfigSummary(config) {
const content = config && typeof config.content === 'string' ? config.content : '';
if (!content.trim()) {
return [];
}
const parsed = this.parseOpenclawContent(content, { allowEmpty: true });
if (!parsed.ok || !isPlainRecord(parsed.data)) {
return [
{ key: 'parse-error', label: '配置状态', value: '解析失败', tone: 'warning' }
];
}
const data = parsed.data;
const agentDefaults = readNestedValue(data, ['agents', 'defaults']);
const modelConfig = isPlainRecord(agentDefaults) ? agentDefaults.model : undefined;
const primaryModel = isPlainRecord(modelConfig)
? modelConfig.primary
: (typeof modelConfig === 'string' ? modelConfig : readNestedValue(data, ['agent', 'model']));
const fallbacks = isPlainRecord(modelConfig) && Array.isArray(modelConfig.fallbacks)
? modelConfig.fallbacks
: [];
const workspace = isPlainRecord(agentDefaults) ? agentDefaults.workspace : '';
const browser = isPlainRecord(data.browser) ? data.browser : {};
const browserMode = browser.enabled === false
? '关闭'
: (browser.headless === true ? 'Headless' : (browser.headless === false ? '可见窗口' : '未声明'));
const executablePath = typeof browser.executablePath === 'string' ? browser.executablePath.trim() : '';
const providerNames = collectDistinctProviderKeys(
readNestedValue(data, ['models', 'providers']),
data.providers
);
return [
{ key: 'primary', label: '默认模型', value: formatOpenclawSummaryValue(primaryModel) },
{ key: 'fallbacks', label: 'Fallback', value: formatOpenclawSummaryValue(fallbacks) },
{ key: 'workspace', label: 'Workspace', value: formatOpenclawSummaryValue(workspace) },
{ key: 'browser', label: 'Browser', value: browserMode },
{ key: 'browser-path', label: 'Chrome', value: formatOpenclawSummaryValue(executablePath) },
{ key: 'providers', label: 'Providers', value: providerNames.length ? providerNames.join(' / ') : '未配置' }
];
},
getOpenclawConfigSummary(config) {
const content = config && typeof config.content === 'string' ? config.content : '';
if (!content.trim()) {
return [];
}
const parsed = this.parseOpenclawContent(content, { allowEmpty: true });
if (!parsed.ok || !isPlainRecord(parsed.data)) {
return [
{ key: 'parse-error', label: '配置状态', value: '解析失败', tone: 'warning' }
];
}
const data = parsed.data;
const agentDefaults = readNestedValue(data, ['agents', 'defaults']);
const modelConfig = isPlainRecord(agentDefaults) ? agentDefaults.model : undefined;
const legacyModel = readNestedValue(data, ['agent', 'model']);
const legacyPrimary = typeof legacyModel === 'string'
? legacyModel
: (isPlainRecord(legacyModel) && typeof legacyModel.primary === 'string' ? legacyModel.primary : undefined);
const primaryModel = isPlainRecord(modelConfig)
? modelConfig.primary
: (typeof modelConfig === 'string' ? modelConfig : legacyPrimary);
const fallbacks = isPlainRecord(modelConfig) && Array.isArray(modelConfig.fallbacks)
? modelConfig.fallbacks
: [];
const workspace = isPlainRecord(agentDefaults) ? agentDefaults.workspace : '';
const browser = isPlainRecord(data.browser) ? data.browser : {};
const browserMode = browser.enabled === false
? '关闭'
: (browser.headless === true ? 'Headless' : (browser.headless === false ? '可见窗口' : '未声明'));
const executablePath = typeof browser.executablePath === 'string' ? browser.executablePath.trim() : '';
const providerNames = collectDistinctProviderKeys(
readNestedValue(data, ['models', 'providers']),
data.providers
);
return [
{ key: 'primary', label: '默认模型', value: formatOpenclawSummaryValue(primaryModel) },
{ key: 'fallbacks', label: 'Fallback', value: formatOpenclawSummaryValue(fallbacks) },
{ key: 'workspace', label: 'Workspace', value: formatOpenclawSummaryValue(workspace) },
{ key: 'browser', label: 'Browser', value: browserMode },
{ key: 'browser-path', label: 'Chrome', value: formatOpenclawSummaryValue(executablePath) },
{ key: 'providers', label: 'Providers', value: providerNames.length ? providerNames.join(' / ') : '未配置' }
];
},
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/modules/app.methods.openclaw-core.mjs` around lines 500 - 538, Update
getOpenclawConfigSummary’s legacy agent.model fallback to handle both string and
plain-object values, using the object’s primary field when present before
passing the result to formatOpenclawSummaryValue. Preserve the existing
agents.defaults model handling and ensure object-form legacy configurations no
longer render as [object Object].

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web-ui/partials/index/modal-config-template-agents.html`:
- Around line 132-137: Update the agents content label conditional in the
template to use a translated `modal.agents.contentLabel.openclawWorkspace` key
with the workspace filename passed via the existing `{fileName}` interpolation
pattern, replacing the hardcoded Chinese text. Add the corresponding key and
interpolation text to every locale file, preserving the existing Claude and
default label branches.

In `@web-ui/res/web-ui-render.precompiled.js`:
- Around line 9240-9243: Replace the hardcoded “内容” suffix in the
openclaw-workspace branch of the agents content label with the localized
`modal.agents.contentLabel.openclawWorkspace` translation, preserving the
workspace filename fallback. Update the corresponding source template
`modal-config-template-agents.html`, regenerate the precompiled output, and
ensure the required key exists consistently in the en, ja, vi, zh-tw, and
Chinese locale files.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0b144264-cc9c-4774-a75e-cb4fcf386fdd

📥 Commits

Reviewing files that changed from the base of the PR and between f1429fa and 3c684cb.

📒 Files selected for processing (6)
  • web-ui/modules/app.methods.agents.mjs
  • web-ui/modules/i18n/locales/zh.mjs
  • web-ui/partials/index/modal-config-template-agents.html
  • web-ui/partials/index/panel-config-openclaw.html
  • web-ui/res/web-ui-render.precompiled.js
  • web-ui/styles/modals-core.css
🚧 Files skipped from review as they are similar to previous changes (1)
  • web-ui/partials/index/panel-config-openclaw.html
📜 Review details
🔇 Additional comments (5)
web-ui/res/web-ui-render.precompiled.js (1)

2256-2269: LGTM!

Also applies to: 2359-2371, 7226-7227, 7338-7339, 9228-9231, 9341-9341

web-ui/partials/index/modal-config-template-agents.html (1)

120-121: LGTM!

web-ui/modules/app.methods.agents.mjs (1)

209-215: LGTM!

web-ui/modules/i18n/locales/zh.mjs (1)

518-520: LGTM!

Also applies to: 945-947, 1517-1521

web-ui/styles/modals-core.css (1)

64-142: LGTM!

Comment on lines +132 to +137
<div class="form-group agents-content-group">
<label class="form-label">
{{ agentsContext === 'openclaw-workspace'
? `${agentsWorkspaceFileName || '工作区文件'} 内容`
: t(agentsContext === 'claude-project' ? 'modal.agents.contentLabel.claudeProject' : 'modal.agents.contentLabel') }}
</label>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect the target template around the cited lines
sed -n '120,150p' web-ui/partials/index/modal-config-template-agents.html

# Find all references to the relevant context and translation keys
rg -n "openclaw-workspace|claude-project|modal\.agents\.contentLabel" web-ui -S

# Locate locale files that define modal.agents.contentLabel keys
fd -a -e mjs -e js -e json -g '*zh*' web-ui
fd -a -e mjs -e js -e json -g '*en*' web-ui

Repository: SakuraByteCore/codexmate

Length of output: 13071


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Inspect i18n implementation for parameter interpolation support
fd -a -e mjs -e js web-ui/modules/i18n
rg -n "function t\\(|const t =|translate\\(|\\{\\s*fileName\\s*\\}|interpolat|replace\\(/\\$\\{|\\{fileName\\}" web-ui/modules/i18n web-ui/modules -S

# Show the relevant locale sections around modal.agents.contentLabel keys
sed -n '530,555p' web-ui/modules/i18n/locales/en.mjs
sed -n '530,555p' web-ui/modules/i18n/locales/zh.mjs
sed -n '535,560p' web-ui/modules/i18n/locales/zh-tw.mjs
sed -n '530,555p' web-ui/modules/i18n/locales/ja.mjs
sed -n '745,770p' web-ui/modules/i18n/locales/vi.mjs

# Look for existing translation keys that use fileName interpolation or similar placeholders
rg -n "fileName|\\{.*\\}" web-ui/modules/i18n/locales -S

Repository: SakuraByteCore/codexmate

Length of output: 544


🏁 Script executed:

#!/bin/bash
set -euo pipefail

# Map the i18n module files
fd . web-ui/modules/i18n

# Inspect the translation helper implementation
ast-grep outline web-ui/modules/i18n --view expanded || true
rg -n "function t\\(|const t =|translate\\(|interpolat|replace\\(/\\$\\{|\\{fileName\\}" web-ui/modules/i18n -S

# Show locale blocks around the existing keys
sed -n '535,555p' web-ui/modules/i18n/locales/en.mjs
sed -n '535,555p' web-ui/modules/i18n/locales/zh.mjs
sed -n '935,950p' web-ui/modules/i18n/locales/zh.mjs
sed -n '935,950p' web-ui/modules/i18n/locales/en.mjs
sed -n '935,950p' web-ui/modules/i18n/locales/zh-tw.mjs

# Search for placeholder-style translations already in use
rg -n "\\{[A-Za-z0-9_]+\\}" web-ui/modules/i18n/locales -S

Repository: SakuraByteCore/codexmate

Length of output: 50380


Localize the openclaw-workspace content label
In web-ui/partials/index/modal-config-template-agents.html, this branch still hardcodes ${agentsWorkspaceFileName || '工作区文件'} 内容 while the other branches use t(). Add a modal.agents.contentLabel.openclawWorkspace key and route this label through the locale files so non-Chinese locales can translate it, matching the existing {fileName} pattern used for the workspace title/hint.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/partials/index/modal-config-template-agents.html` around lines 132 -
137, Update the agents content label conditional in the template to use a
translated `modal.agents.contentLabel.openclawWorkspace` key with the workspace
filename passed via the existing `{fileName}` interpolation pattern, replacing
the hardcoded Chinese text. Add the corresponding key and interpolation text to
every locale file, preserving the existing Claude and default label branches.

Comment on lines +9240 to +9243
_createElementVNode("div", { class: "form-group agents-content-group" }, [
_createElementVNode("label", { class: "form-label" }, _toDisplayString(_ctx.agentsContext === 'openclaw-workspace'
? `${_ctx.agentsWorkspaceFileName || '工作区文件'} 内容`
: _ctx.t(_ctx.agentsContext === 'claude-project' ? 'modal.agents.contentLabel.claudeProject' : 'modal.agents.contentLabel')), 1 /* TEXT */),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Hardcoded Chinese "内容" bypasses i18n for openclaw-workspace content label.

The openclaw-workspace branch uses a template literal ${agentsWorkspaceFileName || '工作区文件'} 内容 while the claude-project and default branches use t(). Non-Chinese locales cannot localize the "内容" suffix. This is inconsistent with setAgentsModalContext which uses tr() with translation keys for title and hint in the same context.

This is flagged on the precompiled output; the source template in modal-config-template-agents.html lines 132-137 has the same issue.

Verify whether other locale files (en, ja, vi, zh-tw) define a modal.agents.contentLabel.openclawWorkspace key:

#!/bin/bash
# Check if any locale file defines the openclawWorkspace content label key
rg -n 'modal\.agents\.contentLabel\.openclawWorkspace' --type=js web-ui/modules/i18n/locales/
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/res/web-ui-render.precompiled.js` around lines 9240 - 9243, Replace
the hardcoded “内容” suffix in the openclaw-workspace branch of the agents content
label with the localized `modal.agents.contentLabel.openclawWorkspace`
translation, preserving the workspace filename fallback. Update the
corresponding source template `modal-config-template-agents.html`, regenerate
the precompiled output, and ensure the required key exists consistently in the
en, ja, vi, zh-tw, and Chinese locale files.

@awsl233777 awsl233777 changed the title fix(openai-bridge): hide retry counts and harden retries fix(codexmate): harden bridge retries and OpenClaw UX Jul 13, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
web-ui/partials/index/panel-config-openclaw.html (1)

14-30: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Hardcoded Chinese strings bypass i18n in the OpenClaw summary card.

Lines 14, 19, 21, and 30 contain hardcoded Chinese text instead of using t() translation calls, breaking localization for non-Chinese locales. This is inconsistent with the rest of the template which uses t() throughout (e.g., lines 79, 95). The aria-label on line 14 is particularly impactful — screen readers in non-Chinese locales will read untranslated Chinese text.

🌐 Proposed fix: use t() for all hardcoded strings
-<section class="settings-card settings-card--wide openclaw-summary-card" aria-label="OpenClaw 当前摘要">
+<section class="settings-card settings-card--wide openclaw-summary-card" :aria-label="t('openclaw.summary.ariaLabel')">
                         <div class="settings-card-body">
                             <div class="openclaw-summary-header">
                                 <div>
                                     <div class="summary-eyebrow">OPENCLAW</div>
-                                    <div class="summary-title">配置与本地诊断</div>
+                                    <div class="summary-title">{{ t('openclaw.summary.title') }}</div>
                                 </div>
-                                <div class="summary-note">配置摘要</div>
+                                <div class="summary-note">{{ t('openclaw.summary.note') }}</div>
                             </div>
                             <div class="openclaw-summary-grid">
                                 <div v-for="item in getOpenclawStatusSummaryItems()" :key="item.key" :class="['openclaw-summary-item', item.tone || '']">
                                     <div class="summary-item-label">{{ item.label }}</div>
                                     <div class="summary-item-value">{{ item.value }}</div>
                                 </div>
                             </div>
                             <div class="openclaw-diagnostic-note">
-                                本页仅管理配置与工作区文件。
+                                {{ t('openclaw.summary.diagnostic') }}
                             </div>
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/partials/index/panel-config-openclaw.html` around lines 14 - 30,
Replace the hardcoded Chinese strings in the OpenClaw summary card—its section
aria-label, summary title, summary note, and diagnostic note—with the existing
t() translation helper, adding or reusing appropriate translation keys while
preserving the current markup and displayed meaning.
web-ui/res/web-ui-render.precompiled.js (1)

9227-9230: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Localize the openclaw-workspace label
The openclaw-workspace branch still appends hardcoded 内容; route that branch through a t() key so the label stays consistent with the other cases.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/res/web-ui-render.precompiled.js` around lines 9227 - 9230, The label
rendering in the agents-content-group branch should localize the
openclaw-workspace case instead of appending the hardcoded 内容 text. Update the
agentsContext conditional to use the existing t() translation mechanism with an
appropriate workspace-content key, while preserving the workspace filename
fallback and the existing claude-project and default label branches.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@web-ui/partials/index/panel-config-openclaw.html`:
- Around line 42-59: Replace the hardcoded “Workspace file” labels with the
localized openclaw.workspace.file key. Update
web-ui/partials/index/panel-config-openclaw.html lines 42-59 to use t() in both
locations, add the key to all locale files, and regenerate or synchronously
update web-ui/res/web-ui-render.precompiled.js lines 2283 and 2306 to use
_ctx.t(...) with the required text patch flag.
- Line 42: Replace the hardcoded “Workspace file” labels in the template and its
precompiled render function with the appropriate t() localization keys,
restoring t('openclaw.agents.hint') for the agent metadata and using the
matching localized key for quick workspace file buttons. Update both occurrences
consistently, including the loading/non-loading expression at the agent metadata
element.

---

Outside diff comments:
In `@web-ui/partials/index/panel-config-openclaw.html`:
- Around line 14-30: Replace the hardcoded Chinese strings in the OpenClaw
summary card—its section aria-label, summary title, summary note, and diagnostic
note—with the existing t() translation helper, adding or reusing appropriate
translation keys while preserving the current markup and displayed meaning.

In `@web-ui/res/web-ui-render.precompiled.js`:
- Around line 9227-9230: The label rendering in the agents-content-group branch
should localize the openclaw-workspace case instead of appending the hardcoded
内容 text. Update the agentsContext conditional to use the existing t()
translation mechanism with an appropriate workspace-content key, while
preserving the workspace filename fallback and the existing claude-project and
default label branches.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5a87021a-f9c1-46eb-9217-196f8d924fa8

📥 Commits

Reviewing files that changed from the base of the PR and between 3c684cb and aec7088.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (5)
  • package.json
  • web-ui/modules/app.methods.openclaw-core.mjs
  • web-ui/partials/index/panel-config-openclaw.html
  • web-ui/res/web-ui-render.precompiled.js
  • web-ui/styles/openclaw-structured.css
🚧 Files skipped from review as they are similar to previous changes (2)
  • web-ui/modules/app.methods.openclaw-core.mjs
  • web-ui/styles/openclaw-structured.css
📜 Review details
🔇 Additional comments (1)
package.json (1)

3-3: LGTM!

<div class="tool-content">
<div class="tool-title">AGENTS.md</div>
<div class="tool-meta">{{ agentsLoading ? t('config.modelLoading') : t('openclaw.agents.hint') }}</div>
<div class="tool-meta">{{ agentsLoading ? t('config.modelLoading') : 'Workspace file' }}</div>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Hardcoded "Workspace file" bypasses i18n — line 42 is a regression from t('openclaw.agents.hint').

Line 42 replaces the previously localized t('openclaw.agents.hint') with a hardcoded 'Workspace file' string, and line 59 uses static Workspace file text for quick workspace file buttons. Both should use t() for consistency with the rest of the template. The same hardcoded strings appear in the precompiled render function at lines 2283 and 2306 — see consolidated comment.

🌐 Proposed fix
-                                        <div class="tool-meta">{{ agentsLoading ? t('config.modelLoading') : 'Workspace file' }}</div>
+                                        <div class="tool-meta">{{ agentsLoading ? t('config.modelLoading') : t('openclaw.workspace.file') }}</div>
                                     </div>
                                     <svg class="tool-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
                                         <path d="M9 18l6-6-6-6"/>
                                     </svg>
                                 </button>

                                 <button
                                     v-for="fileName in getOpenclawQuickWorkspaceFiles()"
                                     :key="fileName"
                                     type="button"
                                     class="openclaw-tool-btn"
                                     `@click`="openOpenclawQuickWorkspaceFile(fileName)"
                                     :disabled="loading || !!initError || agentsLoading">
                                     <div class="tool-icon">📄</div>
                                     <div class="tool-content">
                                         <div class="tool-title">{{ fileName }}</div>
-                                        <div class="tool-meta">Workspace file</div>
+                                        <div class="tool-meta">{{ t('openclaw.workspace.file') }}</div>

Also applies to: 59-59

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/partials/index/panel-config-openclaw.html` at line 42, Replace the
hardcoded “Workspace file” labels in the template and its precompiled render
function with the appropriate t() localization keys, restoring
t('openclaw.agents.hint') for the agent metadata and using the matching
localized key for quick workspace file buttons. Update both occurrences
consistently, including the loading/non-loading expression at the agent metadata
element.

Comment on lines +42 to +59
<div class="tool-meta">{{ agentsLoading ? t('config.modelLoading') : 'Workspace file' }}</div>
</div>
<svg class="tool-chevron" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M9 18l6-6-6-6"/>
</svg>
</button>

<div class="openclaw-workspace-card">
<label class="workspace-label" for="openclaw-workspace-file">{{ t('openclaw.workspaceFile') }}</label>
<div class="workspace-input-group">
<input
id="openclaw-workspace-file"
class="form-input"
v-model="openclawWorkspaceFileName"
:placeholder="t('openclaw.workspace.placeholder')">
<button class="btn-tool" @click="openOpenclawWorkspaceEditor" :disabled="loading || !!initError || agentsLoading">
{{ agentsLoading ? t('config.modelLoading') : t('openclaw.workspace.open') }}
</button>
<button
v-for="fileName in getOpenclawQuickWorkspaceFiles()"
:key="fileName"
type="button"
class="openclaw-tool-btn"
@click="openOpenclawQuickWorkspaceFile(fileName)"
:disabled="loading || !!initError || agentsLoading">
<div class="tool-icon">📄</div>
<div class="tool-content">
<div class="tool-title">{{ fileName }}</div>
<div class="tool-meta">Workspace file</div>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Hardcoded "Workspace file" bypasses i18n across template and precompiled render. The shared root cause is a hardcoded English string that should use t() for localization — line 42 in the template is a regression from the previously localized t('openclaw.agents.hint').

  • web-ui/partials/index/panel-config-openclaw.html#L42-L59: Replace 'Workspace file' on line 42 with t('openclaw.workspace.file') and the static Workspace file on line 59 with {{ t('openclaw.workspace.file') }}. Add the openclaw.workspace.file key to all locale files.
  • web-ui/res/web-ui-render.precompiled.js#L2283-L2283: Replace 'Workspace file' in the ternary with _ctx.t('openclaw.workspace.file') (or regenerate from the fixed template).
  • web-ui/res/web-ui-render.precompiled.js#L2306-L2306: Replace the static "Workspace file" node with _toDisplayString(_ctx.t('openclaw.workspace.file')) and add the 1 /* TEXT */ patch flag (or regenerate from the fixed template).
📍 Affects 2 files
  • web-ui/partials/index/panel-config-openclaw.html#L42-L59 (this comment)
  • web-ui/res/web-ui-render.precompiled.js#L2283-L2283
  • web-ui/res/web-ui-render.precompiled.js#L2306-L2306
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@web-ui/partials/index/panel-config-openclaw.html` around lines 42 - 59,
Replace the hardcoded “Workspace file” labels with the localized
openclaw.workspace.file key. Update
web-ui/partials/index/panel-config-openclaw.html lines 42-59 to use t() in both
locations, add the key to all locale files, and regenerate or synchronously
update web-ui/res/web-ui-render.precompiled.js lines 2283 and 2306 to use
_ctx.t(...) with the required text patch flag.

@ymkiux ymkiux merged commit 7a54654 into main Jul 13, 2026
12 checks passed
@ymkiux ymkiux deleted the fix/builtin-proxy-sub2api-hot-path branch July 13, 2026 13:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants