Merge branch 'master' into gitea/new_feature
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
*/
|
||||
|
||||
|
||||
Reference in New Issue
Block a user