openclaw - 💡(How to fix) Fix [Bug] Gateway Not Displaying Responses from OmniRoute - 'No reply from agent' [1 participants]

Official PRs (…)
ON THIS PAGE

Recommended Tools

×6

Utilities matched from this issue’s tags and category — try them while you read without losing context.

GitHub issue graph ai analysis

Paste a GitHub issue URL. We fetch that issue, discover linked issues from bodies/comments/timeline, collect linked pull requests, and produce a structured English report.

The report is written in English Markdown for sharing and archival.

Helpful · Quick feedback

Loading…
GitHub stats
openclaw/openclaw#55057Fetched 2026-04-08 01:33:09
View on GitHub
Comments
0
Participants
1
Timeline
1
Reactions
0
Author
Participants
Timeline (top)
renamed ×1

Error Message

❌ "No reply from agent" error Summary: The bug is that custom OpenAI-compatible providers (OmniRoute) use streamSimple which emits raw provider events (text_delta, done), but OpenClaw's event handler expects session events (message_start, message_update, message_end). This causes the response content to never be extracted and populated into the payloads array, resulting in the "No reply from agent" error despite the LLM response being received successfully.

Root Cause

Technical Deep Dive: Root Cause Analysis

Fix Action

Fix / Workaround

Solution 3: Immediate Workaround (User-Level)

Solution 4: Use Direct API Calls (Current Workaround)

AspectDetails
SeverityHigh - Completely breaks agent functionality for custom providers
ScopeAll custom/OpenAI-compatible providers using streamSimple
Affected Versions2026.3.24 (confirmed), likely all versions with custom provider support
Workaround AvailableYes - Direct API calls or provider reconfiguration

Code Example

if (!payloads || payloads.length === 0) {
    runtime.log("No reply from agent.");
    return {
        payloads: [],
        meta: result.meta
    };
}

---

return {
       payloads: payloads.length ? payloads : void 0,  // Returns undefined if empty!
       meta: { ... }
   };

---

const payloads = result.payloads ?? [];  // Becomes [] if result.payloads is undefined
   return await deliverAgentCommandResult({
       ...,
       result,
       payloads  // Empty array when it shouldn't be
   });

---

if (!payloads || payloads.length === 0) {
       runtime.log("No reply from agent.");  // BUG TRIGGERED HERE
   }

---

Provider Response
[Custom Stream Wrapper] ← translates to session events
{ type: "message_start", message: {...} }
{ type: "message_update", message: {...}, assistantMessageEvent: {...} }
{ type: "message_end", message: {...} }
Session.subscribe()Event Handler
handleMessageUpdate()pushAssistantText() → assistantTexts[]
buildEmbeddedRunPayloads() → payloads[] populated
Response displayed to user

---

Provider Response
[streamSimple]NO translation, raw provider events
{ type: "text_delta", delta: "Hello" }
{ type: "done", reason: "stop" }
Session.subscribe()Event Handler
No handler for "text_delta" or "done" events
assistantTexts[] remains empty
buildEmbeddedRunPayloads() → payloads[] empty
"No reply from agent" error

---

} else if (params.model.provider === "anthropic-vertex") {
    activeSession.agent.streamFn = createAnthropicVertexStreamFnForModel(params.model);
} else {
    // All other providers including custom/OpenAI-compatible
    activeSession.agent.streamFn = streamSimple;  // ← NO event translation!
}

---

const sessionUnsubscribe = params.session.subscribe(createEmbeddedPiSessionEventHandler(ctx));

---

function createEmbeddedPiSessionEventHandler(ctx) {
    return (evt) => {
        switch (evt.type) {
            case "message_start": handleMessageStart(ctx, evt); return;
            case "message_update": handleMessageUpdate(ctx, evt); return;  // Expects assistantMessageEvent
            case "message_end": handleMessageEnd(ctx, evt); return;
            // ... other cases
            default: return;  // Raw provider events fall through here!
        }
    };
}

---

function handleMessageUpdate(ctx, evt) {
    const msg = evt.message;
    if (msg?.role !== "assistant" || isTranscriptOnlyOpenClawAssistantMessage(msg)) return;
    ctx.noteLastAssistant(msg);
    // ...
    const assistantEvent = evt.assistantMessageEvent;  // ← Required field!
    const assistantRecord = assistantEvent && typeof assistantEvent === "object" ? assistantEvent : void 0;
    const evtType = typeof assistantRecord?.type === "string" ? assistantRecord.type : "";
    // ...
}

---

const pushAssistantText = (text) => {
    if (!text) return;
    if (shouldSkipAssistantText(text)) return;
    assistantTexts.push(text);  // ← This never gets called for custom providers!
    rememberAssistantText(text);
};

---

assistantTexts.push(chunk);

---

// src/agents/pi-embedded-runner/custom-provider-wrapper.ts
export function createCustomProviderStreamWrapper(
    baseStreamFn: StreamFn,
    provider: string
): StreamFn {
    return async function* (model, context, options) {
        // Emit session-compatible message_start
        yield {
            type: "message_start",
            message: { role: "assistant" }
        };
        
        let fullContent = "";
        const stream = baseStreamFn(model, context, options);
        
        for await (const event of stream) {
            if (event.type === "text_delta" || event.type === "content") {
                const delta = event.delta || event.text || "";
                fullContent += delta;
                
                // Emit session-compatible message_update
                yield {
                    type: "message_update",
                    message: {
                        role: "assistant",
                        content: [{ type: "text", text: fullContent }]
                    },
                    assistantMessageEvent: {
                        type: "text_delta",
                        delta
                    }
                };
            }
        }
        
        // Emit session-compatible message_end
        yield {
            type: "message_end",
            message: {
                role: "assistant",
                content: [{ type: "text", text: fullContent }],
                stopReason: "stop"
            }
        };
    };
}

---

} else if (params.model.provider === "anthropic-vertex") {
    activeSession.agent.streamFn = createAnthropicVertexStreamFnForModel(params.model);
} else if (params.model.provider !== "openai") {
    // Custom OpenAI-compatible providers need event translation
    activeSession.agent.streamFn = createCustomProviderStreamWrapper(streamSimple, params.provider);
} else {
    activeSession.agent.streamFn = streamSimple;
}

---

function createEmbeddedPiSessionEventHandler(ctx) {
    return (evt) => {
        switch (evt.type) {
            case "message_start": 
                handleMessageStart(ctx, evt);
                return;
            case "message_update": 
                handleMessageUpdate(ctx, evt);
                return;
            case "message_end": 
                handleMessageEnd(ctx, evt);
                return;
            // NEW: Handle raw provider events
            case "text_delta":
                handleRawTextDelta(ctx, evt);  // Add this handler
                return;
            case "done":
                handleRawDone(ctx, evt);  // Add this handler
                return;
            default: 
                return;
        }
    };
}

function handleRawTextDelta(ctx, evt) {
    const text = evt.delta || evt.text || "";
    if (!text) return;
    
    // Accumulate text and push to assistantTexts
    ctx.pushAssistantText(text);
}

function handleRawDone(ctx, evt) {
    // Finalize the message
    ctx.finalizeAssistantTexts({
        text: ctx.getAccumulatedText(),
        addedDuringMessage: true,
        chunkerHasBuffered: false
    });
}

---

{
    "models": {
        "providers": {
            "omniroute": {
                "api": "ollama",
                "baseUrl": "http://127.0.0.1:20128",
                "models": {
                    "ag/claude-opus-4-6-thinking": {
                        "provider": "omniroute"
                    }
                }
            }
        }
    }
}

---

{
    "models": {
        "providers": {
            "openai": {
                "baseUrl": "http://127.0.0.1:20128"
            }
        }
    }
}

---

curl http://127.0.0.1:20128/v1/chat/completions \
  -H "Authorization: Bearer sk-f0c1ddf471008e76-fese6v-712e419d" \
  -d '{
    "model": "omniroute/ag/claude-opus-4-6-thinking",
    "messages": [{"role": "user", "content": "Hello"}]
  }'
RAW_BUFFERClick to expand / collapse

Technical Deep Dive: Root Cause Analysis

Bug Location (Exact Code)

File: dist/pi-embedded-BaSvmUpW.js (bundled/compiled output) Function: deliverAgentCommandResult() at line ~123520 Critical Check: Line ~123611

if (!payloads || payloads.length === 0) {
    runtime.log("No reply from agent.");
    return {
        payloads: [],
        meta: result.meta
    };
}

Data Flow Analysis

The issue occurs in the following flow:

  1. agentCommandInternal calls deliverAgentCommandResult with:

    • result - The full result from runEmbeddedPiAgent
    • payloads - Extracted from result.payloads ?? []
  2. runEmbeddedPiAgent (line ~176033) returns:

    return {
        payloads: payloads.length ? payloads : void 0,  // Returns undefined if empty!
        meta: { ... }
    };
  3. Back in caller (line ~124691):

    const payloads = result.payloads ?? [];  // Becomes [] if result.payloads is undefined
    return await deliverAgentCommandResult({
        ...,
        result,
        payloads  // Empty array when it shouldn't be
    });
  4. deliverAgentCommandResult checks:

    if (!payloads || payloads.length === 0) {
        runtime.log("No reply from agent.");  // BUG TRIGGERED HERE
    }

🎯 ROOT CAUSE IDENTIFIED: Event Mismatch

The Core Problem

Custom providers (OmniRoute) use streamSimple which emits raw provider events (text_delta, done), but createEmbeddedPiSessionEventHandler expects session events (message_start, message_update, message_end).

Event Flow Breakdown

Working Providers (Ollama/Claude):

Provider Response
[Custom Stream Wrapper] ← translates to session events
{ type: "message_start", message: {...} }
{ type: "message_update", message: {...}, assistantMessageEvent: {...} }
{ type: "message_end", message: {...} }
Session.subscribe() → Event Handler
handleMessageUpdate() → pushAssistantText() → assistantTexts[]
buildEmbeddedRunPayloads() → payloads[] populated
✅ Response displayed to user

Broken Providers (OmniRoute via streamSimple):

Provider Response
[streamSimple] ← NO translation, raw provider events
{ type: "text_delta", delta: "Hello" }
{ type: "done", reason: "stop" }
Session.subscribe() → Event Handler
❌ No handler for "text_delta" or "done" events
assistantTexts[] remains empty
buildEmbeddedRunPayloads() → payloads[] empty
❌ "No reply from agent" error

Code Evidence

Line ~174142 - OmniRoute uses streamSimple:

} else if (params.model.provider === "anthropic-vertex") {
    activeSession.agent.streamFn = createAnthropicVertexStreamFnForModel(params.model);
} else {
    // All other providers including custom/OpenAI-compatible
    activeSession.agent.streamFn = streamSimple;  // ← NO event translation!
}

Line ~171912 - Session subscribes to events:

const sessionUnsubscribe = params.session.subscribe(createEmbeddedPiSessionEventHandler(ctx));

Line ~171441 - Event handler only recognizes session events:

function createEmbeddedPiSessionEventHandler(ctx) {
    return (evt) => {
        switch (evt.type) {
            case "message_start": handleMessageStart(ctx, evt); return;
            case "message_update": handleMessageUpdate(ctx, evt); return;  // Expects assistantMessageEvent
            case "message_end": handleMessageEnd(ctx, evt); return;
            // ... other cases
            default: return;  // Raw provider events fall through here!
        }
    };
}

Line ~170275 - handleMessageUpdate requires specific structure:

function handleMessageUpdate(ctx, evt) {
    const msg = evt.message;
    if (msg?.role !== "assistant" || isTranscriptOnlyOpenClawAssistantMessage(msg)) return;
    ctx.noteLastAssistant(msg);
    // ...
    const assistantEvent = evt.assistantMessageEvent;  // ← Required field!
    const assistantRecord = assistantEvent && typeof assistantEvent === "object" ? assistantEvent : void 0;
    const evtType = typeof assistantRecord?.type === "string" ? assistantRecord.type : "";
    // ...
}

Why Built-in Providers Work

ProviderStream FunctionEvent Translation
OllamacreateConfiguredOllamaStreamFn()✅ Translates to session events
ClaudeBuilt-in handling✅ Custom event mapping
OpenAI WebSocketcreateOpenAIWebSocketStreamFn()✅ Translates to session events
OmniRoutestreamSimpleNo translation (raw events)

Where Payloads Should Be Populated

Line ~171613 - pushAssistantText():

const pushAssistantText = (text) => {
    if (!text) return;
    if (shouldSkipAssistantText(text)) return;
    assistantTexts.push(text);  // ← This never gets called for custom providers!
    rememberAssistantText(text);
};

Line ~171807 - Called from event handler (not reached for raw events):

assistantTexts.push(chunk);

SOLUTIONS

Solution 1: Event Translation Wrapper (Recommended Fix)

Add a wrapper function that translates raw provider events to session events:

// src/agents/pi-embedded-runner/custom-provider-wrapper.ts
export function createCustomProviderStreamWrapper(
    baseStreamFn: StreamFn,
    provider: string
): StreamFn {
    return async function* (model, context, options) {
        // Emit session-compatible message_start
        yield {
            type: "message_start",
            message: { role: "assistant" }
        };
        
        let fullContent = "";
        const stream = baseStreamFn(model, context, options);
        
        for await (const event of stream) {
            if (event.type === "text_delta" || event.type === "content") {
                const delta = event.delta || event.text || "";
                fullContent += delta;
                
                // Emit session-compatible message_update
                yield {
                    type: "message_update",
                    message: {
                        role: "assistant",
                        content: [{ type: "text", text: fullContent }]
                    },
                    assistantMessageEvent: {
                        type: "text_delta",
                        delta
                    }
                };
            }
        }
        
        // Emit session-compatible message_end
        yield {
            type: "message_end",
            message: {
                role: "assistant",
                content: [{ type: "text", text: fullContent }],
                stopReason: "stop"
            }
        };
    };
}

Apply in runEmbeddedPiAgent (around line ~174145):

} else if (params.model.provider === "anthropic-vertex") {
    activeSession.agent.streamFn = createAnthropicVertexStreamFnForModel(params.model);
} else if (params.model.provider !== "openai") {
    // Custom OpenAI-compatible providers need event translation
    activeSession.agent.streamFn = createCustomProviderStreamWrapper(streamSimple, params.provider);
} else {
    activeSession.agent.streamFn = streamSimple;
}

Solution 2: Modify Event Handler to Support Raw Events

Add handlers for raw provider events in createEmbeddedPiSessionEventHandler:

function createEmbeddedPiSessionEventHandler(ctx) {
    return (evt) => {
        switch (evt.type) {
            case "message_start": 
                handleMessageStart(ctx, evt);
                return;
            case "message_update": 
                handleMessageUpdate(ctx, evt);
                return;
            case "message_end": 
                handleMessageEnd(ctx, evt);
                return;
            // NEW: Handle raw provider events
            case "text_delta":
                handleRawTextDelta(ctx, evt);  // Add this handler
                return;
            case "done":
                handleRawDone(ctx, evt);  // Add this handler
                return;
            default: 
                return;
        }
    };
}

function handleRawTextDelta(ctx, evt) {
    const text = evt.delta || evt.text || "";
    if (!text) return;
    
    // Accumulate text and push to assistantTexts
    ctx.pushAssistantText(text);
}

function handleRawDone(ctx, evt) {
    // Finalize the message
    ctx.finalizeAssistantTexts({
        text: ctx.getAccumulatedText(),
        addedDuringMessage: true,
        chunkerHasBuffered: false
    });
}

Solution 3: Immediate Workaround (User-Level)

Option A: Configure OmniRoute as Ollama provider

In ~/.openclaw/openclaw.json:

{
    "models": {
        "providers": {
            "omniroute": {
                "api": "ollama",
                "baseUrl": "http://127.0.0.1:20128",
                "models": {
                    "ag/claude-opus-4-6-thinking": {
                        "provider": "omniroute"
                    }
                }
            }
        }
    }
}

This forces OpenClaw to use createConfiguredOllamaStreamFn which has proper event translation.

Option B: Use OpenAI Provider with Custom Base URL

If OmniRoute is truly OpenAI-compatible, try using the built-in openai provider with a custom base URL:

{
    "models": {
        "providers": {
            "openai": {
                "baseUrl": "http://127.0.0.1:20128"
            }
        }
    }
}

Then reference models as openai/gpt-4 (but OmniRoute handles the routing).

Solution 4: Use Direct API Calls (Current Workaround)

Until the bug is fixed, bypass OpenClaw Gateway and call OmniRoute directly:

curl http://127.0.0.1:20128/v1/chat/completions \
  -H "Authorization: Bearer sk-f0c1ddf471008e76-fese6v-712e419d" \
  -d '{
    "model": "omniroute/ag/claude-opus-4-6-thinking",
    "messages": [{"role": "user", "content": "Hello"}]
  }'

📊 Impact Assessment

AspectDetails
SeverityHigh - Completely breaks agent functionality for custom providers
ScopeAll custom/OpenAI-compatible providers using streamSimple
Affected Versions2026.3.24 (confirmed), likely all versions with custom provider support
Workaround AvailableYes - Direct API calls or provider reconfiguration

🔧 Files to Modify (Source)

  1. src/agents/pi-embedded-runner/run.ts (or equivalent)

    • Modify runEmbeddedPiAgent to detect custom providers
    • Apply event translation wrapper
  2. src/agents/pi-embedded-subscribe.ts (or equivalent)

    • Add handlers for raw provider events
    • OR create separate event handler for custom providers
  3. src/agents/pi-embedded-runner/custom-provider-wrapper.ts (new file)

    • Event translation wrapper for custom providers

🧪 Test Cases to Add

  1. Custom Provider Event Flow: Verify that custom providers emit message_start/update/end events
  2. Payload Population: Confirm that responses from custom providers populate the assistantTexts array
  3. End-to-End: Full agent run with custom provider returns content to user

Summary: The bug is that custom OpenAI-compatible providers (OmniRoute) use streamSimple which emits raw provider events (text_delta, done), but OpenClaw's event handler expects session events (message_start, message_update, message_end). This causes the response content to never be extracted and populated into the payloads array, resulting in the "No reply from agent" error despite the LLM response being received successfully.

Key Finding: This is an event translation layer missing for custom providers, NOT a problem with OmniRoute or the response format. The fix requires either adding an event translation wrapper or extending the event handler to support raw provider events.

extent analysis

Fix Plan

To fix the issue, we will implement an event translation wrapper for custom providers. This wrapper will translate raw provider events (text_delta, done) to session events (message_start, message_update, message_end).

Step 1: Create Event Translation Wrapper

Create a new file custom-provider-wrapper.ts with the following code:

export function createCustomProviderStreamWrapper(
    baseStreamFn: StreamFn,
    provider: string
): StreamFn {
    return async function* (model, context, options) {
        // Emit session-compatible message_start
        yield {
            type: "message_start",
            message: { role: "assistant" }
        };
        
        let fullContent = "";
        const stream = baseStreamFn(model, context, options);
        
        for await (const event of stream) {
            if (event.type === "text_delta" || event.type === "content") {
                const delta = event.delta || event.text || "";
                fullContent += delta;
                
                // Emit session-compatible message_update
                yield {
                    type: "message_update",
                    message: {
                        role: "assistant",
                        content: [{ type: "text", text: fullContent }]
                    },
                    assistantMessageEvent: {
                        type: "text_delta",
                        delta
                    }
                };
            }
        }
        
        // Emit session-compatible message_end
        yield {
            type: "message_end",
            message: {
                role: "assistant",
                content: [{ type: "text", text: fullContent }],
                stopReason: "stop"
            }
        };
    };
}

Step 2: Apply Event Translation Wrapper

Modify runEmbeddedPiAgent to detect custom providers and apply the event translation wrapper:

} else if (params.model.provider === "anthropic-vertex") {
    activeSession.agent.streamFn = createAnthropicVertexStreamFnForModel(params.model);
} else if (params.model.provider !== "openai") {
    // Custom OpenAI-compatible providers need event translation
    activeSession.agent.streamFn = createCustomProviderStreamWrapper(streamSimple, params.provider);
} else {
    activeSession.agent.streamFn = streamSimple;
}

Verification

To verify that the fix worked, test the agent with a custom provider and check that the response content is correctly populated into the payloads array.

Extra Tips

  • Make sure to test the fix with different custom providers to ensure that the event translation wrapper works correctly for all of them.
  • Consider adding additional logging or debugging statements to help diagnose any issues that may arise during testing.
  • If you encounter any problems during testing, refer to the original issue body for more information on the bug and its causes.

Vote matrix · Quick signals

Works
Did the solution work? Tap to confirm.
Easy Fix
Was it a quick fix?
Time Saver
Did it save you time?
Blocking
Was it severely blocking?
Common Issue
Are others likely hitting this too?
Flaky / Intermittent
Is it intermittent?
Verified / Reproducible
Can you reproduce it reliably?
Loading…

Still need to ship something?

×6

Another batch ranked right after the header list — different links, same matching logic.

Back to top recommendations

TRENDING