mirror of
https://github.com/CJackHwang/ds2api.git
synced 2026-05-16 22:25:15 +08:00
refactor: unify empty-output retry logic into shared completionruntime package and normalize protocol adapter boundary.
This commit is contained in:
@@ -641,9 +641,9 @@ function upstreamEmptyOutputDetail(contentFilter, _text, thinking) {
|
||||
};
|
||||
}
|
||||
return {
|
||||
status: 429,
|
||||
message: 'Upstream account hit a rate limit and returned empty output.',
|
||||
code: 'upstream_empty_output',
|
||||
status: 503,
|
||||
message: 'Upstream service is unavailable and returned no output.',
|
||||
code: 'upstream_unavailable',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use strict';
|
||||
|
||||
const CDATA_PATTERN = /^<!\[CDATA\[([\s\S]*?)]]>$/i;
|
||||
const CDATA_PATTERN = /^<!\[CDATA\[([\s\S]*?)]](?:>|>)$/i;
|
||||
const XML_ATTR_PATTERN = /\b([a-z0-9_:-]+)\s*=\s*("([^"]*)"|'([^']*)')/gi;
|
||||
const TOOL_MARKUP_NAMES = [
|
||||
{ raw: 'tool_calls', canonical: 'tool_calls' },
|
||||
@@ -102,9 +102,10 @@ function updateCDATAStateLine(inCDATA, line) {
|
||||
let state = inCDATA;
|
||||
while (pos < lower.length) {
|
||||
if (state) {
|
||||
const end = lower.indexOf(']]>', pos);
|
||||
const cdataEnd = findCDATAEnd(lower, pos);
|
||||
const end = cdataEnd.index;
|
||||
if (end < 0) return true;
|
||||
pos = end + ']]>'.length;
|
||||
pos = end + cdataEnd.len;
|
||||
state = false;
|
||||
continue;
|
||||
}
|
||||
@@ -252,8 +253,9 @@ function replaceDSMLToolMarkupOutsideIgnored(text) {
|
||||
const tag = scanToolMarkupTagAt(raw, i);
|
||||
if (tag) {
|
||||
if (tag.dsmlLike) {
|
||||
out += `<${tag.closing ? '/' : ''}${tag.name}${raw.slice(tag.nameEnd, tag.end + 1)}`;
|
||||
if (raw[tag.end] !== '>') {
|
||||
const tail = normalizeToolMarkupTagTailForXML(raw.slice(tag.nameEnd, tag.end + 1));
|
||||
out += `<${tag.closing ? '/' : ''}${tag.name}${tail}`;
|
||||
if (!tail.endsWith('>')) {
|
||||
out += '>';
|
||||
}
|
||||
} else {
|
||||
@@ -409,11 +411,12 @@ function findMatchingXmlEndTagOutsideCDATA(text, tag, from) {
|
||||
|
||||
function skipXmlIgnoredSection(lower, i) {
|
||||
if (lower.startsWith('<![cdata[', i)) {
|
||||
const end = lower.indexOf(']]>', i + '<![cdata['.length);
|
||||
const cdataEnd = findCDATAEnd(lower, i + '<![cdata['.length);
|
||||
const end = cdataEnd.index;
|
||||
if (end < 0) {
|
||||
return { advanced: false, blocked: true, next: i };
|
||||
}
|
||||
return { advanced: true, blocked: false, next: end + ']]>'.length };
|
||||
return { advanced: true, blocked: false, next: end + cdataEnd.len };
|
||||
}
|
||||
if (lower.startsWith('<!--', i)) {
|
||||
const end = lower.indexOf('-->', i + '<!--'.length);
|
||||
@@ -425,6 +428,21 @@ function skipXmlIgnoredSection(lower, i) {
|
||||
return { advanced: false, blocked: false, next: i };
|
||||
}
|
||||
|
||||
function findCDATAEnd(text, from) {
|
||||
const ascii = text.indexOf(']]>', from);
|
||||
const fullwidth = text.indexOf(']]>', from);
|
||||
if (ascii < 0 && fullwidth < 0) {
|
||||
return { index: -1, len: 0 };
|
||||
}
|
||||
if (ascii < 0) {
|
||||
return { index: fullwidth, len: ']]>'.length };
|
||||
}
|
||||
if (fullwidth < 0 || ascii < fullwidth) {
|
||||
return { index: ascii, len: ']]>'.length };
|
||||
}
|
||||
return { index: fullwidth, len: ']]>'.length };
|
||||
}
|
||||
|
||||
function scanToolMarkupTagAt(text, start) {
|
||||
const raw = toStringSafe(text);
|
||||
if (!raw || start < 0 || start >= raw.length || raw[start] !== '<') {
|
||||
@@ -442,7 +460,7 @@ function scanToolMarkupTagAt(text, start) {
|
||||
const prefix = consumeToolMarkupNamePrefix(raw, lower, i);
|
||||
i = prefix.next;
|
||||
const dsmlLike = prefix.dsmlLike;
|
||||
const { name, len } = matchToolMarkupName(lower, i, dsmlLike);
|
||||
const { name, len } = matchToolMarkupName(raw, i, dsmlLike);
|
||||
if (!name) {
|
||||
return null;
|
||||
}
|
||||
@@ -541,7 +559,7 @@ function findPartialToolMarkupStart(text) {
|
||||
}
|
||||
const start = includeDuplicateLeadingLessThan(raw, lastLT);
|
||||
const tail = raw.slice(start);
|
||||
if (tail.includes('>')) {
|
||||
if (tail.includes('>') || tail.includes('>')) {
|
||||
return -1;
|
||||
}
|
||||
return isPartialToolMarkupTagPrefix(tail) ? start : -1;
|
||||
@@ -579,10 +597,10 @@ function isPartialToolMarkupTagPrefix(text) {
|
||||
if (i === raw.length) {
|
||||
return true;
|
||||
}
|
||||
if (hasToolMarkupNamePrefix(lower.slice(i))) {
|
||||
if (hasToolMarkupNamePrefix(raw, i)) {
|
||||
return true;
|
||||
}
|
||||
if ('dsml'.startsWith(lower.slice(i))) {
|
||||
if (normalizedASCIITailAt(raw, i).startsWith('dsml') || 'dsml'.startsWith(normalizedASCIITailAt(raw, i))) {
|
||||
return true;
|
||||
}
|
||||
const next = consumeToolMarkupNamePrefixOnce(raw, lower, i);
|
||||
@@ -614,9 +632,11 @@ function consumeToolMarkupNamePrefixOnce(raw, lower, idx) {
|
||||
if (idx < raw.length && [' ', '\t', '\r', '\n'].includes(raw[idx])) {
|
||||
return { next: idx + 1, ok: true };
|
||||
}
|
||||
if (lower.startsWith('dsml', idx)) {
|
||||
let next = idx + 'dsml'.length;
|
||||
if (next < raw.length && (raw[next] === '-' || raw[next] === '_')) {
|
||||
const dsml = matchNormalizedASCII(raw, idx, 'dsml');
|
||||
if (dsml.ok) {
|
||||
let next = idx + dsml.len;
|
||||
const sep = normalizeFullwidthASCIIChar(raw[next] || '');
|
||||
if (next < raw.length && (sep === '-' || sep === '_')) {
|
||||
next += 1;
|
||||
}
|
||||
return { next, ok: true };
|
||||
@@ -629,12 +649,15 @@ function consumeToolMarkupNamePrefixOnce(raw, lower, idx) {
|
||||
}
|
||||
|
||||
function consumeArbitraryToolMarkupNamePrefix(raw, lower, idx) {
|
||||
if (idx < 0 || idx >= raw.length || !isToolMarkupPrefixSegmentChar(raw[idx])) {
|
||||
const first = consumeToolMarkupPrefixSegment(raw, idx);
|
||||
if (!first.ok) {
|
||||
return { next: idx, ok: false };
|
||||
}
|
||||
let j = idx + 1;
|
||||
while (j < raw.length && isToolMarkupPrefixSegmentChar(raw[j])) {
|
||||
j += 1;
|
||||
let j = first.next;
|
||||
while (j < raw.length) {
|
||||
const segment = consumeToolMarkupPrefixSegment(raw, j);
|
||||
if (!segment.ok) break;
|
||||
j = segment.next;
|
||||
}
|
||||
let k = j;
|
||||
while (k < raw.length && [' ', '\t', '\r', '\n'].includes(raw[k])) {
|
||||
@@ -645,7 +668,7 @@ function consumeArbitraryToolMarkupNamePrefix(raw, lower, idx) {
|
||||
if (next < raw.length && isToolMarkupPipe(raw[next])) {
|
||||
next += 1;
|
||||
ok = true;
|
||||
} else if (next < raw.length && (raw[next] === '_' || raw[next] === '-')) {
|
||||
} else if (next < raw.length && ['_', '-'].includes(normalizeFullwidthASCIIChar(raw[next]))) {
|
||||
next += 1;
|
||||
ok = true;
|
||||
}
|
||||
@@ -655,32 +678,41 @@ function consumeArbitraryToolMarkupNamePrefix(raw, lower, idx) {
|
||||
while (next < raw.length && [' ', '\t', '\r', '\n'].includes(raw[next])) {
|
||||
next += 1;
|
||||
}
|
||||
if (!hasToolMarkupNamePrefix(lower.slice(next))) {
|
||||
if (!hasToolMarkupNamePrefix(raw, next)) {
|
||||
return { next: idx, ok: false };
|
||||
}
|
||||
return { next, ok: true };
|
||||
}
|
||||
|
||||
function isToolMarkupPrefixSegmentChar(ch) {
|
||||
return /^[A-Za-z0-9]$/.test(ch);
|
||||
function consumeToolMarkupPrefixSegment(raw, idx) {
|
||||
if (idx < 0 || idx >= raw.length) {
|
||||
return { next: idx, ok: false };
|
||||
}
|
||||
const ch = normalizeFullwidthASCIIChar(raw[idx]);
|
||||
if (/^[A-Za-z0-9]$/.test(ch)) {
|
||||
return { next: idx + 1, ok: true };
|
||||
}
|
||||
return { next: idx, ok: false };
|
||||
}
|
||||
|
||||
function hasToolMarkupNamePrefix(lowerTail) {
|
||||
function hasToolMarkupNamePrefix(raw, start) {
|
||||
const tail = normalizedASCIITailAt(raw, start);
|
||||
for (const name of TOOL_MARKUP_NAMES) {
|
||||
if (lowerTail.startsWith(name.raw) || name.raw.startsWith(lowerTail)) {
|
||||
if (tail.startsWith(name.raw) || name.raw.startsWith(tail)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function matchToolMarkupName(lower, start, dsmlLike) {
|
||||
function matchToolMarkupName(raw, start, dsmlLike) {
|
||||
for (const name of TOOL_MARKUP_NAMES) {
|
||||
if (name.dsmlOnly && !dsmlLike) {
|
||||
continue;
|
||||
}
|
||||
if (lower.startsWith(name.raw, start)) {
|
||||
return { name: name.canonical, len: name.raw.length };
|
||||
const matched = matchNormalizedASCII(raw, start, name.raw);
|
||||
if (matched.ok) {
|
||||
return { name: name.canonical, len: matched.len };
|
||||
}
|
||||
}
|
||||
return { name: '', len: 0 };
|
||||
@@ -690,17 +722,18 @@ function findXmlTagEnd(text, from) {
|
||||
let quote = '';
|
||||
for (let i = Math.max(0, from || 0); i < text.length; i += 1) {
|
||||
const ch = text[i];
|
||||
const normalized = normalizeFullwidthASCIIChar(ch);
|
||||
if (quote) {
|
||||
if (ch === quote) {
|
||||
if (normalized === quote) {
|
||||
quote = '';
|
||||
}
|
||||
continue;
|
||||
}
|
||||
if (ch === '"' || ch === "'") {
|
||||
quote = ch;
|
||||
if (normalized === '"' || normalized === "'") {
|
||||
quote = normalized;
|
||||
continue;
|
||||
}
|
||||
if (ch === '>') {
|
||||
if (normalized === '>') {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
@@ -711,13 +744,65 @@ function hasXmlTagBoundary(text, idx) {
|
||||
if (idx >= text.length) {
|
||||
return true;
|
||||
}
|
||||
return [' ', '\t', '\n', '\r', '>', '/'].includes(text[idx]);
|
||||
return [' ', '\t', '\n', '\r', '>', '/'].includes(text[idx])
|
||||
|| normalizeFullwidthASCIIChar(text[idx]) === '>';
|
||||
}
|
||||
|
||||
function isSelfClosingXmlTag(startTag) {
|
||||
return toStringSafe(startTag).trim().endsWith('/');
|
||||
}
|
||||
|
||||
function normalizeFullwidthASCIIChar(ch) {
|
||||
if (!ch) {
|
||||
return ch;
|
||||
}
|
||||
const code = ch.charCodeAt(0);
|
||||
if (code >= 0xff01 && code <= 0xff5e) {
|
||||
return String.fromCharCode(code - 0xfee0);
|
||||
}
|
||||
return ch;
|
||||
}
|
||||
|
||||
function normalizedASCIITailAt(raw, start) {
|
||||
let out = '';
|
||||
for (let i = Math.max(0, start || 0); i < raw.length; i += 1) {
|
||||
const ch = normalizeFullwidthASCIIChar(raw[i]).toLowerCase();
|
||||
if (ch.charCodeAt(0) > 0x7f) {
|
||||
break;
|
||||
}
|
||||
out += ch;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function matchNormalizedASCII(raw, start, expected) {
|
||||
let idx = start;
|
||||
for (let j = 0; j < expected.length; j += 1) {
|
||||
if (idx >= raw.length) {
|
||||
return { ok: false, len: 0 };
|
||||
}
|
||||
const ch = normalizeFullwidthASCIIChar(raw[idx]).toLowerCase();
|
||||
if (ch !== expected[j].toLowerCase()) {
|
||||
return { ok: false, len: 0 };
|
||||
}
|
||||
idx += 1;
|
||||
}
|
||||
return { ok: true, len: idx - start };
|
||||
}
|
||||
|
||||
function normalizeToolMarkupTagTailForXML(tail) {
|
||||
let out = '';
|
||||
for (const ch of typeof tail === 'string' ? tail : String(tail || '')) {
|
||||
const normalized = normalizeFullwidthASCIIChar(ch);
|
||||
if (['>', '/', '=', '"', "'"].includes(normalized)) {
|
||||
out += normalized;
|
||||
} else {
|
||||
out += ch;
|
||||
}
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function parseMarkupInput(raw) {
|
||||
const s = toStringSafe(raw).trim();
|
||||
if (!s) {
|
||||
|
||||
Reference in New Issue
Block a user