mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-18 11:24:47 +00:00
fix(providers): harden model catalog response schemas
This commit is contained in:
@@ -43,6 +43,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Providers/videos: reject malformed successful xAI, OpenRouter, and fal video create, poll, and result responses with provider-owned errors instead of raw parser failures or long bogus polling.
|
||||
- Providers/audio: reject malformed successful OpenAI-compatible, ElevenLabs, and Deepgram speech responses with provider-owned errors instead of raw parser failures, wrong-shaped transcripts, or JSON/text bodies treated as audio.
|
||||
- Providers/embeddings: reject malformed successful OpenAI-compatible, Google Gemini, and Amazon Bedrock embedding responses instead of silently returning empty or coerced vectors.
|
||||
- Providers/catalogs: reject malformed successful LM Studio, GitHub Copilot, DeepInfra, Vercel AI Gateway, and Kilocode model-list responses with provider-owned errors instead of raw parser/type failures or silent fallback catalogs.
|
||||
- Trajectory export: skip and report malformed session/runtime JSONL rows in `manifest.json` instead of letting wrong-shaped session rows crash support bundle export.
|
||||
- Voice calls: persist rejected inbound-call replay keys so duplicate carrier webhook retries stay ignored after a Gateway restart.
|
||||
- Config/doctor: copy fallback-enabled channel `allowFrom` entries into explicit `groupAllowFrom` allowlists during `openclaw doctor --fix`, preserving current group access without adding runtime fallback-transition flags.
|
||||
|
||||
@@ -177,6 +177,25 @@ describe("discoverDeepInfraModels", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back without caching malformed successful model list payloads", async () => {
|
||||
const mockFetch = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ data: {} }),
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ data: [makeModelEntry({ id: "recovered/model" })] }),
|
||||
});
|
||||
|
||||
await withFetchPathTest(mockFetch, async () => {
|
||||
expect(await discoverDeepInfraModels()).toStrictEqual(expectedStaticCatalog());
|
||||
expect((await discoverDeepInfraModels()).map((m) => m.id)).toEqual(["recovered/model"]);
|
||||
expect(mockFetch).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
||||
it("caches successful discovery responses only", async () => {
|
||||
const mockFetch = vi
|
||||
.fn()
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import { buildManifestModelProviderConfig } from "openclaw/plugin-sdk/provider-catalog-shared";
|
||||
import { fetchWithTimeout } from "openclaw/plugin-sdk/provider-http";
|
||||
import {
|
||||
fetchWithTimeout,
|
||||
readProviderJsonArrayFieldResponse,
|
||||
} from "openclaw/plugin-sdk/provider-http";
|
||||
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
|
||||
import manifest from "./openclaw.plugin.json" with { type: "json" };
|
||||
@@ -51,10 +54,6 @@ interface DeepInfraModelEntry {
|
||||
metadata: DeepInfraModelMetadata | null;
|
||||
}
|
||||
|
||||
interface DeepInfraModelsResponse {
|
||||
data?: DeepInfraModelEntry[];
|
||||
}
|
||||
|
||||
function parseModality(metadata: DeepInfraModelMetadata): Array<"text" | "image"> {
|
||||
return metadata.tags?.includes("vision") ? ["text", "image"] : ["text"];
|
||||
}
|
||||
@@ -100,6 +99,17 @@ function staticCatalog(): ModelDefinitionConfig[] {
|
||||
return DEEPINFRA_MODEL_CATALOG.map(buildDeepInfraModelDefinition);
|
||||
}
|
||||
|
||||
function asDeepInfraModelEntry(value: unknown): DeepInfraModelEntry {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
||||
throw new Error("DeepInfra model list: malformed JSON response");
|
||||
}
|
||||
const entry = value as Partial<DeepInfraModelEntry>;
|
||||
if (typeof entry.id !== "string") {
|
||||
throw new Error("DeepInfra model list: malformed JSON response");
|
||||
}
|
||||
return value as DeepInfraModelEntry;
|
||||
}
|
||||
|
||||
export async function discoverDeepInfraModels(): Promise<ModelDefinitionConfig[]> {
|
||||
if (process.env.NODE_ENV === "test" || process.env.VITEST) {
|
||||
return staticCatalog();
|
||||
@@ -122,15 +132,16 @@ export async function discoverDeepInfraModels(): Promise<ModelDefinitionConfig[]
|
||||
return staticCatalog();
|
||||
}
|
||||
|
||||
const body = (await response.json()) as DeepInfraModelsResponse;
|
||||
if (!Array.isArray(body.data) || body.data.length === 0) {
|
||||
const data = await readProviderJsonArrayFieldResponse(response, "DeepInfra model list", "data");
|
||||
if (data.length === 0) {
|
||||
log.warn("No models found from DeepInfra API, using static catalog");
|
||||
return staticCatalog();
|
||||
}
|
||||
|
||||
const seen = new Set<string>();
|
||||
const models: ModelDefinitionConfig[] = [];
|
||||
for (const entry of body.data) {
|
||||
for (const rawEntry of data) {
|
||||
const entry = asDeepInfraModelEntry(rawEntry);
|
||||
const id = typeof entry?.id === "string" ? entry.id.trim() : "";
|
||||
if (!id || seen.has(id) || !entry.metadata) {
|
||||
continue;
|
||||
|
||||
@@ -646,6 +646,24 @@ describe("fetchCopilotModelCatalog", () => {
|
||||
).rejects.toThrow(/HTTP 401/);
|
||||
});
|
||||
|
||||
it("throws provider-owned errors for malformed successful /models payloads", async () => {
|
||||
for (const payload of [[], { data: {} }, { data: [null] }]) {
|
||||
const fetchImpl = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => payload,
|
||||
});
|
||||
|
||||
await expect(
|
||||
fetchCopilotModelCatalog({
|
||||
copilotApiToken: "tid=test",
|
||||
baseUrl: "https://api.githubcopilot.com",
|
||||
fetchImpl: fetchImpl as unknown as typeof fetch,
|
||||
}),
|
||||
).rejects.toThrow("Copilot /models: malformed JSON response");
|
||||
}
|
||||
});
|
||||
|
||||
it("rejects empty token / baseUrl synchronously before fetching", async () => {
|
||||
const fetchImpl = vi.fn();
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import type {
|
||||
ProviderRuntimeModel,
|
||||
} from "openclaw/plugin-sdk/core";
|
||||
import { buildCopilotIdeHeaders, COPILOT_INTEGRATION_ID } from "openclaw/plugin-sdk/provider-auth";
|
||||
import { readProviderJsonArrayFieldResponse } from "openclaw/plugin-sdk/provider-http";
|
||||
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { normalizeModelCompat } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { normalizeOptionalLowercaseString } from "openclaw/plugin-sdk/string-coerce-runtime";
|
||||
@@ -191,6 +192,13 @@ function mapCopilotApiModelToDefinition(
|
||||
return definition;
|
||||
}
|
||||
|
||||
function asCopilotApiModelEntry(value: unknown): CopilotApiModelEntry {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
||||
throw new Error("Copilot /models: malformed JSON response");
|
||||
}
|
||||
return value as CopilotApiModelEntry;
|
||||
}
|
||||
|
||||
export type FetchCopilotModelCatalogParams = {
|
||||
/** Short-lived Copilot API token (from `resolveCopilotApiToken`). */
|
||||
copilotApiToken: string;
|
||||
@@ -242,11 +250,11 @@ export async function fetchCopilotModelCatalog(
|
||||
if (!res.ok) {
|
||||
throw new Error(`Copilot /models fetch failed: HTTP ${res.status}`);
|
||||
}
|
||||
const json = (await res.json()) as { data?: CopilotApiModelEntry[] };
|
||||
const data = Array.isArray(json?.data) ? json.data : [];
|
||||
const data = await readProviderJsonArrayFieldResponse(res, "Copilot /models", "data");
|
||||
const seen = new Set<string>();
|
||||
const out: ModelDefinitionConfig[] = [];
|
||||
for (const entry of data) {
|
||||
for (const rawEntry of data) {
|
||||
const entry = asCopilotApiModelEntry(rawEntry);
|
||||
const def = mapCopilotApiModelToDefinition(entry);
|
||||
if (!def) {
|
||||
continue;
|
||||
|
||||
@@ -226,6 +226,19 @@ describe("discoverKilocodeModels (fetch path)", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to static catalog for malformed successful model list payloads", async () => {
|
||||
for (const payload of [[], { data: {} }, { data: [null] }]) {
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
json: () => Promise.resolve(payload),
|
||||
});
|
||||
await withFetchPathTest(mockFetch, async () => {
|
||||
const models = await discoverKilocodeModels();
|
||||
expect(models).toStrictEqual(EXPECTED_STATIC_KILOCODE_MODELS);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
it("ensures kilo/auto is present even when API doesn't return it", async () => {
|
||||
const mockFetch = vi.fn().mockResolvedValue({
|
||||
ok: true,
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { readProviderJsonArrayFieldResponse } from "openclaw/plugin-sdk/provider-http";
|
||||
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
|
||||
import {
|
||||
@@ -70,10 +71,6 @@ interface GatewayModelEntry {
|
||||
supported_parameters?: string[];
|
||||
}
|
||||
|
||||
interface GatewayModelsResponse {
|
||||
data: GatewayModelEntry[];
|
||||
}
|
||||
|
||||
function toPricePerMillion(perToken: string | undefined): number {
|
||||
if (!perToken) {
|
||||
return 0;
|
||||
@@ -133,6 +130,30 @@ function buildStaticCatalog(): ModelDefinitionConfig[] {
|
||||
}));
|
||||
}
|
||||
|
||||
function asGatewayModelEntry(value: unknown): GatewayModelEntry {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
||||
throw new Error("Kilocode model list: malformed JSON response");
|
||||
}
|
||||
const entry = value as Partial<GatewayModelEntry>;
|
||||
if (
|
||||
typeof entry.id !== "string" ||
|
||||
typeof entry.pricing !== "object" ||
|
||||
entry.pricing === null ||
|
||||
Array.isArray(entry.pricing)
|
||||
) {
|
||||
throw new Error("Kilocode model list: malformed JSON response");
|
||||
}
|
||||
return value as GatewayModelEntry;
|
||||
}
|
||||
|
||||
function readGatewayModelId(value: unknown): string {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
||||
return "";
|
||||
}
|
||||
const id = (value as Partial<GatewayModelEntry>).id;
|
||||
return typeof id === "string" ? id.trim() : "";
|
||||
}
|
||||
|
||||
export async function discoverKilocodeModels(): Promise<ModelDefinitionConfig[]> {
|
||||
if (process.env.NODE_ENV === "test" || process.env.VITEST) {
|
||||
return buildStaticCatalog();
|
||||
@@ -154,8 +175,12 @@ export async function discoverKilocodeModels(): Promise<ModelDefinitionConfig[]>
|
||||
return buildStaticCatalog();
|
||||
}
|
||||
|
||||
const data = (await response.json()) as GatewayModelsResponse;
|
||||
if (!Array.isArray(data.data) || data.data.length === 0) {
|
||||
const data = await readProviderJsonArrayFieldResponse(
|
||||
response,
|
||||
"Kilocode model list",
|
||||
"data",
|
||||
);
|
||||
if (data.length === 0) {
|
||||
log.warn("No models found from gateway API, using static catalog");
|
||||
return buildStaticCatalog();
|
||||
}
|
||||
@@ -163,15 +188,13 @@ export async function discoverKilocodeModels(): Promise<ModelDefinitionConfig[]>
|
||||
const models: ModelDefinitionConfig[] = [];
|
||||
const discoveredIds = new Set<string>();
|
||||
|
||||
for (const entry of data.data) {
|
||||
if (!entry || typeof entry !== "object") {
|
||||
continue;
|
||||
}
|
||||
const id = typeof entry.id === "string" ? entry.id.trim() : "";
|
||||
if (!id || discoveredIds.has(id)) {
|
||||
continue;
|
||||
}
|
||||
for (const rawEntry of data) {
|
||||
const id = readGatewayModelId(rawEntry);
|
||||
try {
|
||||
const entry = asGatewayModelEntry(rawEntry);
|
||||
if (!id || discoveredIds.has(id)) {
|
||||
continue;
|
||||
}
|
||||
models.push(toModelDefinition(entry));
|
||||
discoveredIds.add(id);
|
||||
} catch (e) {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { createSubsystemLogger } from "openclaw/plugin-sdk/logging-core";
|
||||
import { readProviderJsonArrayFieldResponse } from "openclaw/plugin-sdk/provider-http";
|
||||
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { SELF_HOSTED_DEFAULT_COST } from "openclaw/plugin-sdk/provider-setup";
|
||||
import { fetchWithSsrFGuard, type SsrFPolicy } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
@@ -25,10 +26,6 @@ type FetchLmstudioModelsResult = {
|
||||
error?: unknown;
|
||||
};
|
||||
|
||||
type LmstudioModelsResponseWire = {
|
||||
models?: LmstudioModelWire[];
|
||||
};
|
||||
|
||||
type DiscoverLmstudioModelsParams = {
|
||||
baseUrl: string;
|
||||
apiKey: string;
|
||||
@@ -66,6 +63,13 @@ async function fetchLmstudioEndpoint(params: {
|
||||
};
|
||||
}
|
||||
|
||||
function asLmstudioModelWire(value: unknown): LmstudioModelWire {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
||||
throw new Error("LM Studio model list: malformed JSON response");
|
||||
}
|
||||
return value as LmstudioModelWire;
|
||||
}
|
||||
|
||||
/** Fetches /api/v1/models and reports transport reachability separately from HTTP status. */
|
||||
export async function fetchLmstudioModels(params: {
|
||||
baseUrl?: string;
|
||||
@@ -100,17 +104,15 @@ export async function fetchLmstudioModels(params: {
|
||||
models: [],
|
||||
};
|
||||
}
|
||||
let payload: LmstudioModelsResponseWire;
|
||||
try {
|
||||
// External service payload is untrusted JSON; parse with a permissive wire type.
|
||||
payload = (await response.json()) as LmstudioModelsResponseWire;
|
||||
} catch (cause) {
|
||||
throw new Error("LM Studio model list returned malformed JSON", { cause });
|
||||
}
|
||||
const models = await readProviderJsonArrayFieldResponse(
|
||||
response,
|
||||
"LM Studio model list",
|
||||
"models",
|
||||
);
|
||||
return {
|
||||
reachable: true,
|
||||
status: response.status,
|
||||
models: Array.isArray(payload.models) ? payload.models : [],
|
||||
models: models.map(asLmstudioModelWire),
|
||||
};
|
||||
} finally {
|
||||
await release();
|
||||
|
||||
@@ -313,7 +313,25 @@ describe("lmstudio-models", () => {
|
||||
});
|
||||
|
||||
expect(result.reachable).toBe(false);
|
||||
expect((result.error as Error).message).toBe("LM Studio model list returned malformed JSON");
|
||||
expect((result.error as Error).message).toBe("LM Studio model list: malformed JSON response");
|
||||
});
|
||||
|
||||
it("reports wrong-shaped model list payloads with owned errors", async () => {
|
||||
for (const payload of [[], { models: {} }, { models: [null] }]) {
|
||||
const fetchMock = vi.fn(async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => payload,
|
||||
}));
|
||||
|
||||
const result = await fetchLmstudioModels({
|
||||
baseUrl: "http://localhost:1234/v1",
|
||||
fetchImpl: asFetch(fetchMock),
|
||||
});
|
||||
|
||||
expect(result.reachable).toBe(false);
|
||||
expect((result.error as Error).message).toBe("LM Studio model list: malformed JSON response");
|
||||
}
|
||||
});
|
||||
|
||||
it("skips model load when already loaded", async () => {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { readProviderJsonArrayFieldResponse } from "openclaw/plugin-sdk/provider-http";
|
||||
import type { ModelDefinitionConfig } from "openclaw/plugin-sdk/provider-model-shared";
|
||||
import { createSubsystemLogger } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/ssrf-runtime";
|
||||
@@ -33,10 +34,6 @@ type VercelGatewayModelShape = {
|
||||
pricing?: VercelPricingShape;
|
||||
};
|
||||
|
||||
type VercelGatewayModelsResponse = {
|
||||
data?: VercelGatewayModelShape[];
|
||||
};
|
||||
|
||||
type StaticVercelGatewayModel = Omit<ModelDefinitionConfig, "cost"> & {
|
||||
cost?: Partial<ModelDefinitionConfig["cost"]>;
|
||||
};
|
||||
@@ -186,6 +183,13 @@ function buildDiscoveredModelDefinition(
|
||||
};
|
||||
}
|
||||
|
||||
function asVercelGatewayModelShape(value: unknown): VercelGatewayModelShape {
|
||||
if (typeof value !== "object" || value === null || Array.isArray(value)) {
|
||||
throw new Error("Vercel AI Gateway model list: malformed JSON response");
|
||||
}
|
||||
return value as VercelGatewayModelShape;
|
||||
}
|
||||
|
||||
export async function discoverVercelAiGatewayModels(): Promise<ModelDefinitionConfig[]> {
|
||||
if (process.env.VITEST || process.env.NODE_ENV === "test") {
|
||||
return getStaticVercelAiGatewayModelCatalog();
|
||||
@@ -202,8 +206,13 @@ export async function discoverVercelAiGatewayModels(): Promise<ModelDefinitionCo
|
||||
log.warn(`Failed to discover Vercel AI Gateway models: HTTP ${response.status}`);
|
||||
return getStaticVercelAiGatewayModelCatalog();
|
||||
}
|
||||
const data = (await response.json()) as VercelGatewayModelsResponse;
|
||||
const discovered = (data.data ?? [])
|
||||
const data = await readProviderJsonArrayFieldResponse(
|
||||
response,
|
||||
"Vercel AI Gateway model list",
|
||||
"data",
|
||||
);
|
||||
const discovered = data
|
||||
.map(asVercelGatewayModelShape)
|
||||
.map(buildDiscoveredModelDefinition)
|
||||
.filter((entry): entry is ModelDefinitionConfig => entry !== null);
|
||||
return discovered.length > 0 ? discovered : getStaticVercelAiGatewayModelCatalog();
|
||||
|
||||
@@ -1,5 +1,18 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { getStaticVercelAiGatewayModelCatalog, VERCEL_AI_GATEWAY_BASE_URL } from "./api.js";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const { fetchWithSsrFGuardMock } = vi.hoisted(() => ({
|
||||
fetchWithSsrFGuardMock: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock("openclaw/plugin-sdk/ssrf-runtime", () => ({
|
||||
fetchWithSsrFGuard: fetchWithSsrFGuardMock,
|
||||
}));
|
||||
|
||||
import {
|
||||
discoverVercelAiGatewayModels,
|
||||
getStaticVercelAiGatewayModelCatalog,
|
||||
VERCEL_AI_GATEWAY_BASE_URL,
|
||||
} from "./api.js";
|
||||
import {
|
||||
buildStaticVercelAiGatewayProvider,
|
||||
buildVercelAiGatewayProvider,
|
||||
@@ -12,6 +25,31 @@ const STATIC_MODEL_IDS = [
|
||||
"moonshotai/kimi-k2.6",
|
||||
];
|
||||
|
||||
function restoreEnvVar(name: "NODE_ENV" | "VITEST", value: string | undefined): void {
|
||||
if (value === undefined) {
|
||||
delete process.env[name];
|
||||
} else {
|
||||
process.env[name] = value;
|
||||
}
|
||||
}
|
||||
|
||||
async function withLiveDiscovery<T>(run: () => Promise<T>): Promise<T> {
|
||||
const oldNodeEnv = process.env.NODE_ENV;
|
||||
const oldVitest = process.env.VITEST;
|
||||
delete process.env.NODE_ENV;
|
||||
delete process.env.VITEST;
|
||||
try {
|
||||
return await run();
|
||||
} finally {
|
||||
restoreEnvVar("NODE_ENV", oldNodeEnv);
|
||||
restoreEnvVar("VITEST", oldVitest);
|
||||
}
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
fetchWithSsrFGuardMock.mockReset();
|
||||
});
|
||||
|
||||
describe("vercel ai gateway provider catalog", () => {
|
||||
it("builds the bundled Vercel AI Gateway defaults", async () => {
|
||||
const provider = await buildVercelAiGatewayProvider();
|
||||
@@ -36,4 +74,23 @@ describe("vercel ai gateway provider catalog", () => {
|
||||
models: getStaticVercelAiGatewayModelCatalog(),
|
||||
});
|
||||
});
|
||||
|
||||
it("falls back to the static catalog for malformed successful model list payloads", async () => {
|
||||
for (const payload of [[], { data: {} }, { data: [null] }]) {
|
||||
fetchWithSsrFGuardMock.mockResolvedValueOnce({
|
||||
response: {
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => payload,
|
||||
},
|
||||
release: async () => {},
|
||||
});
|
||||
|
||||
await withLiveDiscovery(async () => {
|
||||
expect(await discoverVercelAiGatewayModels()).toStrictEqual(
|
||||
getStaticVercelAiGatewayModelCatalog(),
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -184,6 +184,19 @@ export async function readProviderJsonObjectResponse(
|
||||
return object;
|
||||
}
|
||||
|
||||
export async function readProviderJsonArrayFieldResponse(
|
||||
response: Response,
|
||||
label: string,
|
||||
field: string,
|
||||
): Promise<unknown[]> {
|
||||
const payload = await readProviderJsonObjectResponse(response, label);
|
||||
const value = payload[field];
|
||||
if (!Array.isArray(value)) {
|
||||
throw new Error(`${label}: malformed JSON response`);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizeContentType(response: Response): string | undefined {
|
||||
const contentType = response.headers.get("content-type")?.split(";")[0]?.trim().toLowerCase();
|
||||
return contentType || undefined;
|
||||
|
||||
@@ -11,6 +11,7 @@ export {
|
||||
formatProviderErrorPayload,
|
||||
formatProviderHttpErrorMessage,
|
||||
readProviderBinaryResponse,
|
||||
readProviderJsonArrayFieldResponse,
|
||||
readProviderJsonObjectResponse,
|
||||
readProviderJsonResponse,
|
||||
readResponseTextLimited,
|
||||
|
||||
Reference in New Issue
Block a user