Merge branch 'master' into gitea/new_feature

This commit is contained in:
2026-07-30 23:14:05 +07:00
252 changed files with 20302 additions and 4337 deletions

View File

@@ -291,12 +291,16 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
// Execute request
let providerResponse, providerUrl, providerHeaders, finalBody;
// Most executors return their registry format. Cursor AgentService is an
// exception: it is decoded by the executor into OpenAI-compatible output.
let providerResponseFormat = targetFormat;
try {
const result = await executor.execute({ model, body: translatedBody, stream, credentials, signal: streamController.signal, log, proxyOptions });
providerResponse = result.response;
providerUrl = result.url;
providerHeaders = result.headers;
finalBody = result.transformedBody;
providerResponseFormat = result.responseFormat || targetFormat;
reqLogger.logTargetRequest(providerUrl, providerHeaders, finalBody);
} catch (error) {
trackPendingRequest(model, provider, connectionId, false, true);
@@ -326,7 +330,18 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
// Handle 401/403 - try token refresh (skip for noAuth providers)
if (!executor.noAuth && (providerResponse.status === HTTP_STATUS.UNAUTHORIZED || providerResponse.status === HTTP_STATUS.FORBIDDEN)) {
try {
const newCredentials = await refreshWithRetry(() => executor.refreshCredentials(credentials, log), 3, log);
// Mutate credentials after each successful refresh: rotating refresh_token
// providers (xAI/grok-cli) issue a new RT on every refresh; without this,
// refreshWithRetry's 2nd/3rd attempt reuses the already-consumed RT →
// invalid_grant → auth_failed retryable=false.
const newCredentials = await refreshWithRetry(async () => {
const result = await executor.refreshCredentials(credentials, log);
if (result?.refreshToken && result.refreshToken !== credentials.refreshToken) {
if (result.accessToken) credentials.accessToken = result.accessToken;
credentials.refreshToken = result.refreshToken;
}
return result;
}, 3, log);
if (newCredentials?.accessToken || newCredentials?.copilotToken) {
if (log?.line) log.line(reqTag, "🔑", `TOKEN REFRESHED · ${provider}/${model}`);
Object.assign(credentials, newCredentials);
@@ -335,7 +350,11 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
}
try {
const retryResult = await executor.execute({ model, body: translatedBody, stream, credentials, signal: streamController.signal, log, proxyOptions });
if (retryResult.response.ok) { providerResponse = retryResult.response; providerUrl = retryResult.url; }
if (retryResult.response.ok) {
providerResponse = retryResult.response;
providerUrl = retryResult.url;
providerResponseFormat = retryResult.responseFormat || targetFormat;
}
} catch { log?.warn?.("TOKEN", `${provider.toUpperCase()} | retry after refresh failed`); }
} else {
log?.warn?.("TOKEN", `${provider.toUpperCase()} | refresh failed`);
@@ -382,14 +401,14 @@ export async function handleChatCore({ body, modelInfo, credentials, log, onCred
// True non-streaming response
if (!stream) {
const result = await handleNonStreamingResponse({ ...sharedCtx, providerResponse, sourceFormat, targetFormat, reqLogger, toolNameMap, trackDone, appendLog });
const result = await handleNonStreamingResponse({ ...sharedCtx, providerResponse, sourceFormat, targetFormat: providerResponseFormat, reqLogger, toolNameMap, trackDone, appendLog });
streamController.handleComplete();
return result;
}
// Streaming response
const { onStreamComplete, streamDetailId } = buildOnStreamComplete({ ...sharedCtx });
return handleStreamingResponse({ ...sharedCtx, providerResponse, sourceFormat, targetFormat, userAgent, reqLogger, toolNameMap, streamController, onStreamComplete, streamDetailId });
return handleStreamingResponse({ ...sharedCtx, providerResponse, sourceFormat, targetFormat: providerResponseFormat, userAgent, reqLogger, toolNameMap, streamController, onStreamComplete, streamDetailId });
}
export function isTokenExpiringSoon(expiresAt, bufferMs = 5 * 60 * 1000) {

View File

@@ -40,15 +40,21 @@ function pickAssistantMessageForChatCompletion(output) {
*/
export function parseSSEToOpenAIResponse(rawSSE, fallbackModel) {
const chunks = [];
let streamError = null;
for (const line of String(rawSSE || "").split("\n")) {
const trimmed = line.trim();
if (!trimmed.startsWith("data:")) continue;
const payload = trimmed.slice(5).trim();
if (!payload || payload === "[DONE]") continue;
try { chunks.push(JSON.parse(payload)); } catch { /* ignore malformed lines */ }
try {
const chunk = JSON.parse(payload);
if (chunk?.error) streamError = chunk.error;
else chunks.push(chunk);
} catch { /* ignore malformed lines */ }
}
if (streamError) return { error: streamError };
if (chunks.length === 0) return null;
const first = chunks[0];
@@ -196,6 +202,12 @@ export async function handleForcedSSEToJson({ providerResponse, sourceFormat, pr
const sseText = await providerResponse.text();
const parsed = parseSSEToOpenAIResponse(sseText, model);
if (!parsed) return createErrorResult(HTTP_STATUS.BAD_GATEWAY, "Invalid SSE response for non-streaming request");
if (parsed.error) {
return createErrorResult(
HTTP_STATUS.BAD_GATEWAY,
parsed.error.message || "Upstream SSE stream failed"
);
}
if (onRequestSuccess) await onRequestSuccess();

View File

@@ -116,6 +116,7 @@ export async function handleEmbeddingsCore({
return {
success: true,
usage: normalized.usage || null,
response: new Response(JSON.stringify(normalized), {
headers: {
"Content-Type": "application/json",

View File

@@ -49,7 +49,10 @@ function truncate(text, max) {
}
function parseJinaTitle(text) {
const m = String(text || "").match(/^\s*#\s+(.+)$/m);
const source = String(text || "");
const metadataTitle = source.match(/^\s*Title:\s*(.+)$/mi);
if (metadataTitle) return metadataTitle[1].trim();
const m = source.match(/^\s*#\s+(.+)$/m);
return m ? m[1].trim() : null;
}
@@ -151,11 +154,14 @@ async function runFirecrawl({ url, fmt, timeoutMs, apiKey, maxCharacters, costPe
}
async function runJina({ url, fmt, timeoutMs, apiKey, maxCharacters, costPerQuery, startedAt }) {
const target = `https://r.jina.ai/${encodeURIComponent(url)}`;
const upstreamStart = Date.now();
const r = await tryFetch(target, {
method: "GET",
headers: apiKey ? { authorization: `Bearer ${apiKey}` } : {}
const r = await tryFetch("https://r.jina.ai/", {
method: "POST",
headers: {
"content-type": "application/json",
...(apiKey ? { authorization: `Bearer ${apiKey}` } : {})
},
body: JSON.stringify({ url })
}, timeoutMs);
if (!r.ok) {

View File

@@ -1,7 +1,6 @@
/**
* Search Provider Request Builders
*
* Ported from OmniRoute open-sse/handlers/search.ts (lines 223-610).
* Builds HTTP request `{ url, init }` for 10 search providers.
*
* @typedef {Object} SearchProviderConfig

View File

@@ -1,7 +1,6 @@
/**
* Search Response Normalizers
*
* Ported from OmniRoute open-sse/handlers/search.ts.
* Each normalizer maps a provider-specific response into the unified SearchResult shape.
*/