refactor: extract provider HTTP helpers to provider-request.ts

Moves RequestUrlFunction type, endpointUrl, responseJson, requestJsonBody,
shouldRetryWithoutStructuredOutput, and requestJsonBodyWithStructuredFallback
to a dedicated provider-request.ts module (103 lines). providers.ts reduced
from 377 to 277 lines.

Change-Id: I013d6a0b9565770c00e48eb416541fcf08d1c5e8
This commit is contained in:
wujunchen 2026-04-27 15:41:28 +08:00
parent ed93e25cc9
commit 60e9ad6bb2
3 changed files with 114 additions and 111 deletions

20
main.js

File diff suppressed because one or more lines are too long

103
src/provider-request.ts Normal file
View file

@ -0,0 +1,103 @@
'use strict';
import { translate } from './i18n';
import type { PluginSettings } from './types';
export type RequestUrlFunction = (params: {
url: string;
method: string;
headers: Record<string, string>;
body: string;
throw?: boolean;
}) => Promise<{ status: number; json: unknown; text: string }>;
export function endpointUrl(baseUrl: string, suffixes: string[]) {
const base = baseUrl.replace(/\/+$/, '');
for (const suffix of suffixes) {
if (base.endsWith(suffix)) return base;
}
return base + suffixes[0];
}
export function responseJson(
resp: { json: unknown; text: string },
label: string,
settings?: PluginSettings | null,
): Record<string, unknown> {
if (resp.json && typeof resp.json === 'object') return resp.json as Record<string, unknown>;
try {
return JSON.parse(resp.text || '{}') as Record<string, unknown>;
} catch (_) {
throw new Error(
translate(settings || null, 'errorProviderNonJson', {
label,
excerpt: (resp.text || '').slice(0, 500),
}),
);
}
}
export async function requestJsonBody(
requestUrlImpl: RequestUrlFunction,
label: string,
url: string,
headers: Record<string, string>,
body: unknown,
settings?: PluginSettings | null,
): Promise<Record<string, unknown>> {
let resp: { status: number; json: unknown; text: string };
try {
resp = await requestUrlImpl({
url,
method: 'POST',
headers,
body: JSON.stringify(body),
throw: false,
});
} catch (e: unknown) {
throw new Error(
translate(settings || null, 'errorProviderRequestFailed', {
label,
error: e instanceof Error ? e.message : String(e),
}),
);
}
if (resp.status >= 400) {
throw new Error(
translate(settings || null, 'errorProviderApiStatus', {
label,
status: resp.status,
excerpt: (resp.text || '').slice(0, 500),
}),
);
}
return responseJson(resp, label, settings);
}
export function shouldRetryWithoutStructuredOutput(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error);
if (!/(?:API (?:400|404|422):|API returned HTTP (?:400|404|422)|API 返回 HTTP (?:400|404|422))/.test(message))
return false;
return /response_format|json_schema|responseJsonSchema|responseMimeType|tools?|tool_choice|unsupported|unrecognized|unknown|schema/i.test(
message,
);
}
export async function requestJsonBodyWithStructuredFallback(
requestUrlImpl: RequestUrlFunction,
label: string,
url: string,
headers: Record<string, string>,
structuredBody: unknown,
fallbackBody: unknown,
settings?: PluginSettings | null,
): Promise<Record<string, unknown>> {
try {
return await requestJsonBody(requestUrlImpl, label, url, headers, structuredBody, settings);
} catch (e: unknown) {
if (!fallbackBody || !shouldRetryWithoutStructuredOutput(e)) throw e;
console.warn(`[parallel-reader] ${label} structured output rejected; retrying without structured output`, e);
return requestJsonBody(requestUrlImpl, label + ' fallback', url, headers, fallbackBody, settings);
}
}

View file

@ -14,6 +14,7 @@ import {
textFromOpenAiChatResponse,
textFromOpenAiResponsesResponse,
} from './provider-parsers';
import { endpointUrl, type RequestUrlFunction, requestJsonBodyWithStructuredFallback } from './provider-request';
import { parseCardsJson } from './schema';
import {
API_FORMATS,
@ -35,16 +36,6 @@ export {
tokenLimitFieldForOpenAiChat,
} from './provider-bodies';
/* ---------- Typed request/response shapes ---------- */
type RequestUrlFunction = (params: {
url: string;
method: string;
headers: Record<string, string>;
body: string;
throw?: boolean;
}) => Promise<{ status: number; json: unknown; text: string }>;
function requiredDeltaExtractor(format: string) {
const extractor = deltaExtractorForFormat(format);
if (!extractor) throw new Error(`Streaming not supported for format: ${format}`);
@ -53,14 +44,6 @@ function requiredDeltaExtractor(format: string) {
/* ---------- Helpers ---------- */
function endpointUrl(baseUrl: string, suffixes: string[]) {
const base = baseUrl.replace(/\/+$/, '');
for (const suffix of suffixes) {
if (base.endsWith(suffix)) return base;
}
return base + suffixes[0];
}
export function parseApiHeaders(raw: string, settings?: PluginSettings | null): Record<string, string> {
const text = (raw || '').trim();
if (!text) return {};
@ -125,89 +108,6 @@ function buildApiHeaders(settings: PluginSettings, extra?: Record<string, string
};
}
function responseJson(
resp: { json: unknown; text: string },
label: string,
settings?: PluginSettings | null,
): Record<string, unknown> {
if (resp.json && typeof resp.json === 'object') return resp.json as Record<string, unknown>;
try {
return JSON.parse(resp.text || '{}') as Record<string, unknown>;
} catch (_) {
throw new Error(
translate(settings || null, 'errorProviderNonJson', {
label,
excerpt: (resp.text || '').slice(0, 500),
}),
);
}
}
async function requestJsonBody(
requestUrlImpl: RequestUrlFunction,
label: string,
url: string,
headers: Record<string, string>,
body: unknown,
settings?: PluginSettings | null,
): Promise<Record<string, unknown>> {
let resp: { status: number; json: unknown; text: string };
try {
resp = await requestUrlImpl({
url,
method: 'POST',
headers,
body: JSON.stringify(body),
throw: false,
});
} catch (e: unknown) {
throw new Error(
translate(settings || null, 'errorProviderRequestFailed', {
label,
error: e instanceof Error ? e.message : String(e),
}),
);
}
if (resp.status >= 400) {
throw new Error(
translate(settings || null, 'errorProviderApiStatus', {
label,
status: resp.status,
excerpt: (resp.text || '').slice(0, 500),
}),
);
}
return responseJson(resp, label, settings);
}
function shouldRetryWithoutStructuredOutput(error: unknown): boolean {
const message = error instanceof Error ? error.message : String(error);
if (!/(?:API (?:400|404|422):|API returned HTTP (?:400|404|422)|API 返回 HTTP (?:400|404|422))/.test(message))
return false;
return /response_format|json_schema|responseJsonSchema|responseMimeType|tools?|tool_choice|unsupported|unrecognized|unknown|schema/i.test(
message,
);
}
async function requestJsonBodyWithStructuredFallback(
requestUrlImpl: RequestUrlFunction,
label: string,
url: string,
headers: Record<string, string>,
structuredBody: unknown,
fallbackBody: unknown,
settings?: PluginSettings | null,
): Promise<Record<string, unknown>> {
try {
return await requestJsonBody(requestUrlImpl, label, url, headers, structuredBody, settings);
} catch (e: unknown) {
if (!fallbackBody || !shouldRetryWithoutStructuredOutput(e)) throw e;
console.warn(`[parallel-reader] ${label} structured output rejected; retrying without structured output`, e);
return requestJsonBody(requestUrlImpl, label + ' fallback', url, headers, fallbackBody, settings);
}
}
async function summarizeViaAnthropicMessages(
requestUrlImpl: RequestUrlFunction,
system: string,