mirror of
https://github.com/moltbot/moltbot.git
synced 2026-05-13 15:47:28 +00:00
fix(model): repair provider replay edge cases
This commit is contained in:
@@ -137,6 +137,8 @@ Docs: https://docs.openclaw.ai
|
||||
### Fixes
|
||||
|
||||
- CLI backends: keep versioned OAuth identity matches reusable when auth profile ids rotate, so Claude CLI sessions do not reset and lose continuity during same-account OAuth refresh/profile alias changes. Fixes #78541.
|
||||
- Model providers: normalize APNG sniffed PNG uploads, preserve Gemini 3 tool-call thought-signature replay with documented fallback signatures, accept legacy `__env__:VAR` custom-provider keys, and repair snake_case tool-call transcript sanitization. Fixes #51881, #48915, #77566, and #42858.
|
||||
- Telegram/models: parse provider ids containing dots in `/models` callback buttons so `hf.co` model lists render as inline keyboard buttons. Fixes #38745.
|
||||
- Anthropic: reject uppercase provider-prefixed forward-compat model ids locally instead of sending malformed dynamic ids upstream. Fixes #73715.
|
||||
- OpenAI/embeddings: pass configured output dimensionality through single and batched embedding requests so memory embedding indexes can request smaller vectors. Fixes #55126.
|
||||
- CLI/infer: normalize HEIC/HEIF image files to JPEG before model-run requests, avoiding providers that reject Apple image container formats. Fixes #50081.
|
||||
|
||||
@@ -504,6 +504,44 @@ describe("google transport stream", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("uses Gemini skip-validator thought signatures for cross-provider tool-call replay", () => {
|
||||
const model = buildGeminiModel({
|
||||
id: "gemini-3.1-pro-preview",
|
||||
name: "Gemini 3.1 Pro Preview",
|
||||
});
|
||||
|
||||
const params = buildGoogleGenerativeAiParams(model, {
|
||||
messages: [
|
||||
{
|
||||
role: "assistant",
|
||||
provider: "anthropic",
|
||||
api: "anthropic-messages",
|
||||
model: "claude-opus-4-7",
|
||||
stopReason: "toolUse",
|
||||
timestamp: 0,
|
||||
content: [
|
||||
{
|
||||
type: "toolCall",
|
||||
id: "call_1",
|
||||
name: "lookup",
|
||||
arguments: { q: "hello" },
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
} as never);
|
||||
|
||||
expect(params.contents[0]).toMatchObject({
|
||||
role: "model",
|
||||
parts: [
|
||||
{
|
||||
thoughtSignature: "skip_thought_signature_validator",
|
||||
functionCall: { name: "lookup", args: { q: "hello" } },
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("builds direct Gemini payloads without negative fallback thinking budgets", () => {
|
||||
const model = {
|
||||
id: "custom-gemini-model",
|
||||
|
||||
@@ -134,6 +134,7 @@ type GoogleSseChunk = {
|
||||
};
|
||||
|
||||
let toolCallCounter = 0;
|
||||
const GEMINI_THOUGHT_SIGNATURE_VALIDATOR_SKIP = "skip_thought_signature_validator";
|
||||
|
||||
function normalizeOptionalString(value: unknown): string | undefined {
|
||||
return typeof value === "string" && value.trim() ? value.trim() : undefined;
|
||||
@@ -143,6 +144,10 @@ function requiresToolCallId(modelId: string): boolean {
|
||||
return modelId.startsWith("claude-") || modelId.startsWith("gpt-oss-");
|
||||
}
|
||||
|
||||
function requiresToolCallThoughtSignature(modelId: string): boolean {
|
||||
return normalizeLowercaseStringOrEmpty(modelId).includes("gemini-3");
|
||||
}
|
||||
|
||||
function supportsMultimodalFunctionResponse(modelId: string): boolean {
|
||||
const match = normalizeLowercaseStringOrEmpty(modelId).match(/^gemini(?:-live)?-(\d+)/);
|
||||
if (!match) {
|
||||
@@ -377,8 +382,11 @@ function normalizeGoogleThinkingConfig(
|
||||
|
||||
function convertGoogleMessages(model: GoogleTransportModel, context: Context) {
|
||||
const contents: Array<Record<string, unknown>> = [];
|
||||
const transformedMessages = transformTransportMessages(context.messages, model, (id) =>
|
||||
requiresToolCallId(model.id) ? normalizeToolCallId(id) : id,
|
||||
const transformedMessages = transformTransportMessages(
|
||||
context.messages,
|
||||
model,
|
||||
(id) => (requiresToolCallId(model.id) ? normalizeToolCallId(id) : id),
|
||||
{ preserveCrossModelToolCallThoughtSignature: true },
|
||||
);
|
||||
for (const msg of transformedMessages) {
|
||||
if (msg.role === "user") {
|
||||
@@ -440,15 +448,18 @@ function convertGoogleMessages(model: GoogleTransportModel, context: Context) {
|
||||
continue;
|
||||
}
|
||||
if (block.type === "toolCall") {
|
||||
const thoughtSignature =
|
||||
(isSameProviderAndModel ? block.thoughtSignature : undefined) ??
|
||||
(requiresToolCallThoughtSignature(model.id)
|
||||
? GEMINI_THOUGHT_SIGNATURE_VALIDATOR_SKIP
|
||||
: undefined);
|
||||
parts.push({
|
||||
functionCall: {
|
||||
name: block.name,
|
||||
args: coerceTransportToolCallArguments(block.arguments),
|
||||
...(requiresToolCallId(model.id) ? { id: block.id } : {}),
|
||||
},
|
||||
...(isSameProviderAndModel && block.thoughtSignature
|
||||
? { thoughtSignature: block.thoughtSignature }
|
||||
: {}),
|
||||
...(thoughtSignature ? { thoughtSignature } : {}),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ describe("parseModelCallbackData", () => {
|
||||
["mdl_back", { type: "back" }],
|
||||
["mdl_list_anthropic_2", { type: "list", provider: "anthropic", page: 2 }],
|
||||
["mdl_list_open-ai_1", { type: "list", provider: "open-ai", page: 1 }],
|
||||
["mdl_list_hf.co_1", { type: "list", provider: "hf.co", page: 1 }],
|
||||
[
|
||||
"mdl_sel_anthropic/claude-sonnet-4-5",
|
||||
{ type: "select", provider: "anthropic", model: "claude-sonnet-4-5" },
|
||||
|
||||
@@ -63,7 +63,7 @@ export function parseModelCallbackData(data: string): ParsedModelCallback | null
|
||||
}
|
||||
|
||||
// mdl_list_{provider}_{page}
|
||||
const listMatch = trimmed.match(/^mdl_list_([a-z0-9_-]+)_(\d+)$/i);
|
||||
const listMatch = trimmed.match(/^mdl_list_([a-z0-9_.-]+)_(\d+)$/i);
|
||||
if (listMatch) {
|
||||
const [, provider, pageStr] = listMatch;
|
||||
const page = Number.parseInt(pageStr ?? "1", 10);
|
||||
|
||||
@@ -2,6 +2,29 @@ import { describe, expect, it, vi } from "vitest";
|
||||
import { resolveRemoteEmbeddingBearerClient } from "./embeddings-remote-client.js";
|
||||
|
||||
describe("resolveRemoteEmbeddingBearerClient", () => {
|
||||
it("uses configured OpenAI provider baseUrl for memory embeddings", async () => {
|
||||
const client = await resolveRemoteEmbeddingBearerClient({
|
||||
provider: "openai",
|
||||
defaultBaseUrl: "https://api.openai.com/v1",
|
||||
options: {
|
||||
agentDir: "/tmp/openclaw-agent",
|
||||
config: {
|
||||
models: {
|
||||
providers: {
|
||||
openai: {
|
||||
apiKey: "sk-config",
|
||||
baseUrl: "https://proxy.example.test/openai/v1",
|
||||
},
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
model: "text-embedding-3-small",
|
||||
},
|
||||
});
|
||||
|
||||
expect(client.baseUrl).toBe("https://proxy.example.test/openai/v1");
|
||||
});
|
||||
|
||||
it("adds OpenClaw attribution to native OpenAI embedding requests", async () => {
|
||||
vi.stubEnv("OPENCLAW_VERSION", "2026.3.22");
|
||||
const client = await resolveRemoteEmbeddingBearerClient({
|
||||
|
||||
@@ -518,6 +518,36 @@ describe("resolveUsableCustomProviderApiKey", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("resolves legacy __env__ markers from process env for custom providers", () => {
|
||||
const previous = process.env.BAILIAN_API_KEY;
|
||||
process.env.BAILIAN_API_KEY = "sk-bailian-env"; // pragma: allowlist secret
|
||||
try {
|
||||
const resolved = resolveUsableCustomProviderApiKey({
|
||||
cfg: {
|
||||
models: {
|
||||
providers: {
|
||||
bailian: {
|
||||
baseUrl: "https://coding.dashscope.aliyuncs.com/v1",
|
||||
api: "openai-completions",
|
||||
apiKey: "__env__:BAILIAN_API_KEY", // pragma: allowlist secret
|
||||
models: [],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
provider: "bailian",
|
||||
});
|
||||
expect(resolved?.apiKey).toBe("sk-bailian-env");
|
||||
expect(resolved?.source).toContain("BAILIAN_API_KEY");
|
||||
} finally {
|
||||
if (previous === undefined) {
|
||||
delete process.env.BAILIAN_API_KEY;
|
||||
} else {
|
||||
process.env.BAILIAN_API_KEY = previous;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it("does not resolve env SecretRefs when provider allowlist excludes the env id", () => {
|
||||
const previous = process.env.MY_CUSTOM_KEY;
|
||||
process.env.MY_CUSTOM_KEY = "sk-custom-secretref-env"; // pragma: allowlist secret
|
||||
|
||||
@@ -2901,7 +2901,7 @@ describe("openai transport stream", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("does not replay thought_signature across a different API surface", () => {
|
||||
it("uses the Gemini skip-validator signature across a different API surface", () => {
|
||||
const params = buildOpenAICompletionsParams(
|
||||
geminiModel,
|
||||
{
|
||||
@@ -2938,12 +2938,14 @@ describe("openai transport stream", () => {
|
||||
) as { messages: Array<Record<string, unknown>> };
|
||||
|
||||
const assistant = params.messages.find((message) => message.role === "assistant") as
|
||||
| { tool_calls?: Array<{ extra_content?: unknown }> }
|
||||
| { tool_calls?: Array<{ extra_content?: { google?: { thought_signature?: string } } }> }
|
||||
| undefined;
|
||||
expect(assistant?.tool_calls?.[0]?.extra_content).toBeUndefined();
|
||||
expect(assistant?.tool_calls?.[0]?.extra_content?.google?.thought_signature).toBe(
|
||||
"skip_thought_signature_validator",
|
||||
);
|
||||
});
|
||||
|
||||
it("does not emit extra_content when no thought_signature was captured", () => {
|
||||
it("uses the Gemini skip-validator signature when no thought_signature was captured", () => {
|
||||
const params = buildOpenAICompletionsParams(
|
||||
geminiModel,
|
||||
{
|
||||
@@ -2972,9 +2974,11 @@ describe("openai transport stream", () => {
|
||||
) as { messages: Array<Record<string, unknown>> };
|
||||
|
||||
const assistant = params.messages.find((message) => message.role === "assistant") as
|
||||
| { tool_calls?: Array<{ extra_content?: unknown }> }
|
||||
| { tool_calls?: Array<{ extra_content?: { google?: { thought_signature?: string } } }> }
|
||||
| undefined;
|
||||
expect(assistant?.tool_calls?.[0]?.extra_content).toBeUndefined();
|
||||
expect(assistant?.tool_calls?.[0]?.extra_content?.google?.thought_signature).toBe(
|
||||
"skip_thought_signature_validator",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -58,6 +58,7 @@ import { mergeTransportMetadata, sanitizeTransportPayloadText } from "./transpor
|
||||
|
||||
const DEFAULT_AZURE_OPENAI_API_VERSION = "2024-12-01-preview";
|
||||
const OPENAI_CODEX_RESPONSES_EMPTY_INPUT_TEXT = " ";
|
||||
const GEMINI_THOUGHT_SIGNATURE_VALIDATOR_SKIP = "skip_thought_signature_validator";
|
||||
const log = createSubsystemLogger("openai-transport");
|
||||
|
||||
type ReplayableResponseOutputMessage = Omit<ResponseOutputMessage, "id"> & { id?: string };
|
||||
@@ -1800,6 +1801,10 @@ function isGoogleOpenAICompatModel(model: OpenAIModeModel): boolean {
|
||||
);
|
||||
}
|
||||
|
||||
function requiresGoogleCompatToolCallThoughtSignature(model: OpenAIModeModel): boolean {
|
||||
return model.id.toLowerCase().includes("gemini-3");
|
||||
}
|
||||
|
||||
function injectToolCallThoughtSignatures(
|
||||
outgoingMessages: unknown[],
|
||||
context: Context,
|
||||
@@ -1809,18 +1814,14 @@ function injectToolCallThoughtSignatures(
|
||||
return;
|
||||
}
|
||||
const sigById = new Map<string, string>();
|
||||
const fallbackSig = requiresGoogleCompatToolCallThoughtSignature(model)
|
||||
? GEMINI_THOUGHT_SIGNATURE_VALIDATOR_SKIP
|
||||
: undefined;
|
||||
for (const msg of context.messages ?? []) {
|
||||
if ((msg as { role?: string }).role !== "assistant") {
|
||||
continue;
|
||||
}
|
||||
const source = msg as { api?: string; provider?: string; model?: string; content?: unknown };
|
||||
if (
|
||||
source.api !== model.api ||
|
||||
source.provider !== model.provider ||
|
||||
source.model !== model.id
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
if (!Array.isArray(source.content)) {
|
||||
continue;
|
||||
}
|
||||
@@ -1831,11 +1832,15 @@ function injectToolCallThoughtSignatures(
|
||||
const id = block.id;
|
||||
const sig = block.thoughtSignature;
|
||||
if (typeof id === "string" && typeof sig === "string" && sig.length > 0) {
|
||||
sigById.set(id, sig);
|
||||
const isSameRoute =
|
||||
source.api === model.api &&
|
||||
source.provider === model.provider &&
|
||||
source.model === model.id;
|
||||
sigById.set(id, isSameRoute ? sig : (fallbackSig ?? sig));
|
||||
}
|
||||
}
|
||||
}
|
||||
if (sigById.size === 0) {
|
||||
if (sigById.size === 0 && !fallbackSig) {
|
||||
return;
|
||||
}
|
||||
for (const message of outgoingMessages) {
|
||||
@@ -1848,7 +1853,7 @@ function injectToolCallThoughtSignatures(
|
||||
if (typeof id !== "string") {
|
||||
continue;
|
||||
}
|
||||
const sig = sigById.get(id);
|
||||
const sig = sigById.get(id) ?? fallbackSig;
|
||||
if (!sig) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -8,7 +8,14 @@ import {
|
||||
} from "./session-transcript-repair.js";
|
||||
import { castAgentMessage, castAgentMessages } from "./test-helpers/agent-message-fixtures.js";
|
||||
|
||||
const TOOL_CALL_BLOCK_TYPES = new Set(["toolCall", "toolUse", "functionCall"]);
|
||||
const TOOL_CALL_BLOCK_TYPES = new Set([
|
||||
"toolCall",
|
||||
"toolUse",
|
||||
"functionCall",
|
||||
"tool_call",
|
||||
"tool_use",
|
||||
"function_call",
|
||||
]);
|
||||
|
||||
function getAssistantToolCallBlocks(messages: AgentMessage[]) {
|
||||
const assistant = messages[0] as Extract<AgentMessage, { role: "assistant" }> | undefined;
|
||||
@@ -316,6 +323,29 @@ describe("sanitizeToolUseResultPairing", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("sanitizeToolCallInputs", () => {
|
||||
it("drops malformed snake_case tool call blocks", () => {
|
||||
const input = castAgentMessages([
|
||||
{
|
||||
role: "assistant",
|
||||
content: [
|
||||
{ type: "text", text: "before" },
|
||||
{ type: "tool_use", id: "tool_1", name: "read" },
|
||||
{ type: "tool_call", tool_call_id: "tool_2", name: "write", arguments: {} },
|
||||
{ type: "function_call", call_id: "tool_3", name: "exec", arguments: "{}" },
|
||||
],
|
||||
},
|
||||
]);
|
||||
|
||||
const out = sanitizeToolCallInputs(input, { allowedToolNames: ["write", "exec"] });
|
||||
|
||||
expect(getAssistantToolCallBlocks(out)).toMatchObject([
|
||||
{ type: "tool_call", name: "write" },
|
||||
{ type: "function_call", name: "exec" },
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sanitizeToolCallInputs", () => {
|
||||
function sanitizeAssistantContent(
|
||||
content: unknown[],
|
||||
|
||||
@@ -16,11 +16,25 @@ import {
|
||||
type RawToolCallBlock = {
|
||||
type?: unknown;
|
||||
id?: unknown;
|
||||
call_id?: unknown;
|
||||
toolCallId?: unknown;
|
||||
toolUseId?: unknown;
|
||||
tool_call_id?: unknown;
|
||||
tool_use_id?: unknown;
|
||||
name?: unknown;
|
||||
input?: unknown;
|
||||
arguments?: unknown;
|
||||
};
|
||||
|
||||
const RAW_TOOL_CALL_BLOCK_TYPES = new Set([
|
||||
"toolCall",
|
||||
"toolUse",
|
||||
"functionCall",
|
||||
"tool_call",
|
||||
"tool_use",
|
||||
"function_call",
|
||||
]);
|
||||
|
||||
function isThinkingLikeBlock(block: unknown): boolean {
|
||||
if (!block || typeof block !== "object") {
|
||||
return false;
|
||||
@@ -34,10 +48,7 @@ function isRawToolCallBlock(block: unknown): block is RawToolCallBlock {
|
||||
return false;
|
||||
}
|
||||
const type = (block as { type?: unknown }).type;
|
||||
return (
|
||||
typeof type === "string" &&
|
||||
(type === "toolCall" || type === "toolUse" || type === "functionCall")
|
||||
);
|
||||
return typeof type === "string" && RAW_TOOL_CALL_BLOCK_TYPES.has(type);
|
||||
}
|
||||
|
||||
function hasToolCallInput(block: RawToolCallBlock): boolean {
|
||||
@@ -52,7 +63,14 @@ function hasNonEmptyStringField(value: unknown): boolean {
|
||||
}
|
||||
|
||||
function hasToolCallId(block: RawToolCallBlock): boolean {
|
||||
return hasNonEmptyStringField(block.id);
|
||||
return (
|
||||
hasNonEmptyStringField(block.id) ||
|
||||
hasNonEmptyStringField(block.call_id) ||
|
||||
hasNonEmptyStringField(block.toolCallId) ||
|
||||
hasNonEmptyStringField(block.toolUseId) ||
|
||||
hasNonEmptyStringField(block.tool_call_id) ||
|
||||
hasNonEmptyStringField(block.tool_use_id)
|
||||
);
|
||||
}
|
||||
|
||||
function redactSessionsSpawnAttachmentsArgs(value: unknown): unknown {
|
||||
@@ -350,11 +368,7 @@ function repairToolCallInputs(
|
||||
continue;
|
||||
}
|
||||
if (isRawToolCallBlock(block)) {
|
||||
if (
|
||||
(block as { type?: unknown }).type === "toolCall" ||
|
||||
(block as { type?: unknown }).type === "toolUse" ||
|
||||
(block as { type?: unknown }).type === "functionCall"
|
||||
) {
|
||||
if (RAW_TOOL_CALL_BLOCK_TYPES.has((block as { type?: string }).type ?? "")) {
|
||||
// Only sanitize (redact) sessions_spawn blocks; all others are passed through
|
||||
// unchanged to preserve provider-specific shapes (e.g. toolUse.input for Anthropic).
|
||||
const blockName =
|
||||
|
||||
@@ -45,6 +45,7 @@ export function transformTransportMessages(
|
||||
targetModel: Model<Api>,
|
||||
source: { provider: string; api: Api; model: string },
|
||||
) => string,
|
||||
options?: { preserveCrossModelToolCallThoughtSignature?: boolean },
|
||||
): Context["messages"] {
|
||||
const allowSyntheticToolResults = defaultAllowSyntheticToolResults(model.api);
|
||||
const syntheticToolResultText = CODEX_STYLE_ABORTED_OUTPUT_APIS.has(model.api)
|
||||
@@ -94,7 +95,11 @@ export function transformTransportMessages(
|
||||
continue;
|
||||
}
|
||||
let normalizedToolCall = block;
|
||||
if (!isSameModel && block.thoughtSignature) {
|
||||
if (
|
||||
!isSameModel &&
|
||||
block.thoughtSignature &&
|
||||
options?.preserveCrossModelToolCallThoughtSignature !== true
|
||||
) {
|
||||
normalizedToolCall = { ...normalizedToolCall };
|
||||
delete normalizedToolCall.thoughtSignature;
|
||||
}
|
||||
|
||||
@@ -90,6 +90,11 @@ describe("parseLegacySecretRefEnvMarker", () => {
|
||||
provider: "default",
|
||||
id: "OPENAI_API_KEY",
|
||||
});
|
||||
expect(parseLegacySecretRefEnvMarker("__env__:BAILIAN_API_KEY")).toEqual({
|
||||
source: "env",
|
||||
provider: "default",
|
||||
id: "BAILIAN_API_KEY",
|
||||
});
|
||||
expect(parseLegacySecretRefEnvMarker("secretref-env:not-valid")).toBeNull();
|
||||
expect(
|
||||
resolveSecretInputString({
|
||||
|
||||
@@ -19,6 +19,7 @@ export type SecretInput = string | SecretRef;
|
||||
export const DEFAULT_SECRET_PROVIDER_ALIAS = "default"; // pragma: allowlist secret
|
||||
export const ENV_SECRET_REF_ID_RE = /^[A-Z][A-Z0-9_]{0,127}$/;
|
||||
export const LEGACY_SECRETREF_ENV_MARKER_PREFIX = "secretref-env:"; // pragma: allowlist secret
|
||||
export const LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX = "__env__:"; // pragma: allowlist secret
|
||||
const ENV_SECRET_TEMPLATE_RE = /^\$\{([A-Z][A-Z0-9_]{0,127})\}$/;
|
||||
export type SecretInputStringResolutionMode = "strict" | "inspect";
|
||||
export type SecretInputStringResolution =
|
||||
@@ -91,10 +92,15 @@ export function parseLegacySecretRefEnvMarker(
|
||||
return null;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed.startsWith(LEGACY_SECRETREF_ENV_MARKER_PREFIX)) {
|
||||
const prefix = trimmed.startsWith(LEGACY_SECRETREF_ENV_MARKER_PREFIX)
|
||||
? LEGACY_SECRETREF_ENV_MARKER_PREFIX
|
||||
: trimmed.startsWith(LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX)
|
||||
? LEGACY_DOUBLE_UNDERSCORE_ENV_MARKER_PREFIX
|
||||
: undefined;
|
||||
if (!prefix) {
|
||||
return null;
|
||||
}
|
||||
const id = trimmed.slice(LEGACY_SECRETREF_ENV_MARKER_PREFIX.length);
|
||||
const id = trimmed.slice(prefix.length);
|
||||
if (!ENV_SECRET_REF_ID_RE.test(id)) {
|
||||
return null;
|
||||
}
|
||||
@@ -109,6 +115,10 @@ export function coerceSecretRef(value: unknown, defaults?: SecretDefaults): Secr
|
||||
if (isSecretRef(value)) {
|
||||
return value;
|
||||
}
|
||||
const legacyEnvMarker = parseLegacySecretRefEnvMarker(value, defaults?.env);
|
||||
if (legacyEnvMarker) {
|
||||
return legacyEnvMarker;
|
||||
}
|
||||
if (isLegacySecretRefWithoutProvider(value)) {
|
||||
const provider =
|
||||
value.source === "env"
|
||||
|
||||
@@ -207,6 +207,7 @@ describe("normalizeMimeType", () => {
|
||||
|
||||
it.each([
|
||||
{ input: "Audio/MP4; codecs=mp4a.40.2", expected: "audio/mp4" },
|
||||
{ input: "image/apng", expected: "image/png" },
|
||||
{ input: " ", expected: undefined },
|
||||
{ input: null, expected: undefined },
|
||||
{ input: undefined, expected: undefined },
|
||||
|
||||
@@ -74,6 +74,9 @@ export function normalizeMimeType(mime?: string | null): string | undefined {
|
||||
return undefined;
|
||||
}
|
||||
const cleaned = mime.split(";")[0]?.trim().toLowerCase();
|
||||
if (cleaned === "image/apng") {
|
||||
return "image/png";
|
||||
}
|
||||
return cleaned || undefined;
|
||||
}
|
||||
|
||||
@@ -93,7 +96,7 @@ async function sniffMime(buffer?: Buffer): Promise<string | undefined> {
|
||||
const { fileTypeFromBuffer } = await fileTypeModuleLoader.load();
|
||||
const type = await fileTypeFromBuffer(sliceMimeSniffBuffer(buffer));
|
||||
if (type?.mime) {
|
||||
return type.mime;
|
||||
return normalizeMimeType(type.mime);
|
||||
}
|
||||
} catch {
|
||||
// fall through to manual magic-byte sniffs
|
||||
|
||||
Reference in New Issue
Block a user