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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 6 additions & 9 deletions open-sse/handlers/chatCore/nonStreamingHandler.js
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import { FORMATS } from "../../translator/formats.js";
import { needsTranslation } from "../../translator/index.js";
import { ollamaBodyToOpenAI } from "../../translator/response/ollama-to-openai.js";
import { addBufferToUsage, filterUsageForFormat } from "../../utils/usageTracking.js";
import { createErrorResult } from "../../utils/error.js";
import { HTTP_STATUS } from "../../config/runtimeConfig.js";
import { HTTP_STATUS } from "../../config/constants.js";
import { parseSSEToOpenAIResponse } from "./sseToJsonHandler.js";
import { buildRequestDetail, extractRequestConfig, extractUsageFromResponse, saveUsageStats } from "./requestDetail.js";
import { appendRequestLog, saveRequestDetail } from "@/lib/usageDb.js";
Expand Down Expand Up @@ -77,8 +76,11 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
const toolCalls = [];

for (const block of responseBody.content) {
if (block.type === "text") textContent += block.text;
else if (block.type === "thinking") thinkingContent += block.thinking || "";
if (block.type === "text") {
let text = block.text;
text = text.replace(/^\s*```\s*json\s*\n?/i, "").replace(/\n?\s*```\s*$/i, "");
textContent += text;
}
else if (block.type === "tool_use") {
toolCalls.push({ id: block.id, type: "function", function: { name: block.name, arguments: JSON.stringify(block.input || {}) } });
}
Expand Down Expand Up @@ -112,11 +114,6 @@ export function translateNonStreamingResponse(responseBody, targetFormat, source
return result;
}

// Ollama
if (targetFormat === FORMATS.OLLAMA) {
return ollamaBodyToOpenAI(responseBody);
}

return responseBody;
}

Expand Down
9 changes: 4 additions & 5 deletions open-sse/translator/request/openai-to-claude.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { register } from "../index.js";
import { FORMATS } from "../formats.js";
import { CLAUDE_SYSTEM_PROMPT } from "../../config/appConstants.js";
import { CLAUDE_SYSTEM_PROMPT } from "../../config/constants.js";
import { adjustMaxTokens } from "../helpers/maxTokensHelper.js";

// Empty prefix matches real Claude Code behavior (no tool name prefix).
Expand Down Expand Up @@ -106,10 +106,9 @@ export function openaiToClaudeRequest(model, body, stream) {
if (responseFormat.type === "json_schema" && responseFormat.json_schema?.schema) {
const schemaJson = JSON.stringify(responseFormat.json_schema.schema, null, 2);
systemParts.push(`You must respond with valid JSON that strictly follows this JSON schema:
\`\`\`json
${schemaJson}
\`\`\`
Respond ONLY with the JSON object, no other text.`);
Schema: ${schemaJson}

IMPORTANT: Return ONLY the raw JSON object without markdown formatting, code blocks, or backticks.`);
} else if (responseFormat.type === "json_object") {
systemParts.push("You must respond with valid JSON. Respond ONLY with a JSON object, no other text.");
}
Expand Down
5 changes: 4 additions & 1 deletion open-sse/translator/response/claude-to-openai.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,10 @@ export function claudeToOpenAIResponse(chunk, state) {
if (chunk.index === state.serverToolBlockIndex) break;
const delta = chunk.delta;
if (delta?.type === "text_delta" && delta.text) {
results.push(createChunk(state, { content: delta.text }));
let text = delta.text;
// Strip markdown code block markers
text = text.replace(/^\s*```\s*json\s*\n?/i, "").replace(/\n?\s*```\s*$/i, "");
results.push(createChunk(state, { content: text }));
} else if (delta?.type === "thinking_delta" && delta.thinking) {
results.push(createChunk(state, { reasoning_content: delta.thinking }));
} else if (delta?.type === "input_json_delta" && delta.partial_json) {
Expand Down
34 changes: 34 additions & 0 deletions open-sse/utils/jsonExtractor.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Extract JSON from content that may be wrapped in markdown code blocks
* Only processes content when responseFormat is specified
* @param {string} content - Raw response content
* @param {object} responseFormat - The response_format from the request body
* @returns {string} - Extracted JSON or original content
*/
export function extractJSON(content, responseFormat) {
// Only process if response_format is specified
if (!responseFormat || !content || typeof content !== 'string') {
return content;
}

// Check if it's a structured output request
const isStructuredOutput = responseFormat.type === 'json_schema' ||
responseFormat.type === 'json_object';

if (!isStructuredOutput) {
return content;
}

// Try to find JSON between markdown code blocks
const codeBlockMatch = content.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/i);
if (codeBlockMatch && codeBlockMatch[1]) {
const extracted = codeBlockMatch[1].trim();
// Validate it looks like JSON
if (extracted.startsWith('{') || extracted.startsWith('[')) {
return extracted;
}
}

// Return original if no markdown found or doesn't look like JSON
return content;
}
23 changes: 15 additions & 8 deletions open-sse/utils/stream.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { extractJSON } from './jsonExtractor.js';
import { translateResponse, initState } from "../translator/index.js";
import { FORMATS } from "../translator/formats.js";
import { trackPendingRequest, appendRequestLog } from "@/lib/usageDb.js";
Expand Down Expand Up @@ -159,7 +160,7 @@ export function createSSEStream(options = {}) {
// Translate mode
if (!trimmed) continue;

const parsed = parseSSELine(trimmed, targetFormat);
const parsed = parseSSELine(trimmed);
if (!parsed) continue;

if (parsed && parsed.done) {
Expand Down Expand Up @@ -278,13 +279,16 @@ export function createSSEStream(options = {}) {
// Some clients (e.g. OpenClaw) expect the OpenAI-style sentinel:
// data: [DONE]\n\n
// Without it they can hang until timeout and trigger failover.
const doneOutput = "data: [DONE]\n\n";
reqLogger?.appendConvertedChunk?.(doneOutput);
controller.enqueue(sharedEncoder.encode(doneOutput));
// Only emit data: [DONE] for streaming requests
if (body?.stream === true) {
const doneOutput = "data: [DONE]\n\n";
reqLogger?.appendConvertedChunk?.(doneOutput);
controller.enqueue(sharedEncoder.encode(doneOutput));
}

if (onStreamComplete) {
onStreamComplete({
content: accumulatedContent,
content: extractJSON(accumulatedContent, body?.response_format),
thinking: accumulatedThinking
}, usage, ttftAt);
}
Expand Down Expand Up @@ -330,9 +334,12 @@ export function createSSEStream(options = {}) {
}
}

const doneOutput = "data: [DONE]\n\n";
reqLogger?.appendConvertedChunk?.(doneOutput);
// Only emit data: [DONE] for streaming requests
if (body?.stream === true) {
const doneOutput = "data: [DONE]\n\n";
reqLogger?.appendConvertedChunk?.(doneOutput);
controller.enqueue(sharedEncoder.encode(doneOutput));
}

if (!hasValidUsage(state?.usage) && totalContentLength > 0) {
state.usage = estimateUsage(body, totalContentLength, sourceFormat);
Expand All @@ -346,7 +353,7 @@ export function createSSEStream(options = {}) {

if (onStreamComplete) {
onStreamComplete({
content: accumulatedContent,
content: extractJSON(accumulatedContent, body?.response_format),
thinking: accumulatedThinking
}, state?.usage, ttftAt);
}
Expand Down