Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 | 1x 1x 20x 20x 20x 20x 20x 11x 11x 20x 1x 13x 13x 13x 7x 7x 7x 4x 4x 4x 13x 2x 2x 13x 13x 13x 1x 13x 13x 1x 8x 8x 8x 8x 1x 1x 7x 7x 7x 8x 8x 2x 8x 1x 1x 6x 8x 5x 5x 5x 5x 4x 5x 2x 2x 5x 8x 8x | import { FunctionName } from '@shared/types/api/request';
import type { SuperAgentsRequestData } from '@shared/types/api/request/body';
import type { ChatCompletionRequestBody } from '@shared/types/api/routes/chat-completions-api';
/** Matches a markdown code fence and captures whatever sits inside it. */
const FENCED_BLOCK = /```[a-zA-Z0-9_-]*\r?\n?([\s\S]*?)```/g;
function isJson(text: string): boolean {
try {
JSON.parse(text);
return true;
} catch {
return false;
}
}
/**
* Pulls the JSON out of a message that is *about* JSON rather than being JSON:
* a markdown fence, or an object with commentary around it. Returns null when
* the content is already valid JSON, or when nothing in it parses -- in both
* cases the caller should leave the content alone.
*/
export function extractJsonFromContent(content: string): string | null {
const trimmed = content.trim();
if (trimmed === '' || isJson(trimmed)) return null;
for (const match of trimmed.matchAll(FENCED_BLOCK)) {
const fenced = match[1].trim();
if (isJson(fenced)) return fenced;
}
// No usable fence: fall back to the outermost object or array in the text.
const objectStart = trimmed.indexOf('{');
const arrayStart = trimmed.indexOf('[');
const start =
objectStart === -1 || (arrayStart !== -1 && arrayStart < objectStart)
? arrayStart
: objectStart;
if (start === -1) return null;
const end = trimmed.lastIndexOf(trimmed[start] === '{' ? '}' : ']');
if (end <= start) return null;
const candidate = trimmed.slice(start, end + 1);
return isJson(candidate) ? candidate : null;
}
/**
* Providers differ in how seriously they take `response_format`. The ones that
* enforce a schema return bare JSON; the ones that only see the instruction in
* the prompt -- self-hosted models through Ollama, most of all -- routinely
* wrap it in a ```json fence. A client that asked for `json_schema` is entitled
* to content it can hand to `JSON.parse` (the OpenAI SDK's `.parse()` does
* exactly that and throws on the backticks), so the fence is stripped here, at
* the boundary where the gateway makes providers look alike.
*
* Only chat completions that asked for a JSON response format are touched, and
* only when the extracted text really is JSON; anything else is passed through
* untouched so a provider's own reply still reaches the caller verbatim.
*/
export function unwrapJsonResponseContent(
saRequestData: SuperAgentsRequestData,
responseBody: Record<string, unknown>,
): Record<string, unknown> {
if (saRequestData.functionName !== FunctionName.CHAT_COMPLETE) {
return responseBody;
}
const responseFormat = (
saRequestData.requestBody as ChatCompletionRequestBody
)?.response_format;
if (
responseFormat?.type !== 'json_schema' &&
responseFormat?.type !== 'json_object'
) {
return responseBody;
}
const choices = responseBody.choices;
if (!Array.isArray(choices)) return responseBody;
let unwrapped = false;
const nextChoices = choices.map((choice) => {
const message = (choice as { message?: { content?: unknown } })?.message;
if (typeof message?.content !== 'string') return choice;
const json = extractJsonFromContent(message.content);
if (json === null) return choice;
unwrapped = true;
return { ...choice, message: { ...message, content: json } };
});
return unwrapped ? { ...responseBody, choices: nextChoices } : responseBody;
}
|