mirror of
https://github.com/morluto/rea
synced 2026-10-26 06:16:17 -04:00
fix(model): preserve unsupported GGUF version diagnostics (#5789)
Preserve unsupported GGUF version bytes and both UINT32 interpretations without inventing byte order at the shared parser. Keep existing layout support, typed errors, evidence identity, scoped reads and resource limits. Validation: 78 focused tests, full static/architecture and documentation checks, real CLI/MCP version controls and an independently checked public model. Latest-head Codex found no issues; Greptile is 5/5 and all review threads are resolved. Some CI remained pending at merge qualification. Merge under the human owner's explicit standing bypass authorization does not claim the full CI suite passed. Fixes #5749.
This commit is contained in:
@@ -5,6 +5,11 @@ GGUF v2/v3 or safetensors file. Detection uses content, including GGUF byte
|
||||
order, rather than its extension. REA does not load a model, interpret weights,
|
||||
run inference, import Python, open external-data references or use the network.
|
||||
|
||||
Unsupported GGUF versions remain refused. Their diagnostic preserves the four
|
||||
version bytes and both UINT32 interpretations with byte order unknown, rather
|
||||
than inferring big-endian from an unrecognized little-endian value. Little-endian
|
||||
v2/v3 and big-endian v3 remain supported; big-endian v2 remains refused.
|
||||
|
||||
```bash
|
||||
rea inspect-model-artifact /absolute/path/model.gguf --json
|
||||
rea inspect-model-artifact /absolute/path/model.safetensors --hash full --json
|
||||
|
||||
+12
-7
@@ -247,16 +247,21 @@ export class GgufHeader {
|
||||
tensors: ModelTensor[];
|
||||
}> {
|
||||
const versionBytes = await this.reader.take(4);
|
||||
this.#little =
|
||||
versionBytes.readUInt32LE() === 2 || versionBytes.readUInt32LE() === 3;
|
||||
const version = this.#little
|
||||
? versionBytes.readUInt32LE()
|
||||
: versionBytes.readUInt32BE();
|
||||
if (version !== 2 && version !== 3)
|
||||
const littleVersion = versionBytes.readUInt32LE();
|
||||
const bigVersion = versionBytes.readUInt32BE();
|
||||
let version: number;
|
||||
if (littleVersion === 2 || littleVersion === 3) {
|
||||
this.#little = true;
|
||||
version = littleVersion;
|
||||
} else if (bigVersion === 2 || bigVersion === 3) {
|
||||
this.#little = false;
|
||||
version = bigVersion;
|
||||
} else {
|
||||
throw new ModelFormatFailure(
|
||||
"unsupported",
|
||||
`GGUF version ${String(version)} is unsupported; supported versions are 2 and 3`,
|
||||
`GGUF version bytes ${versionBytes.toString("hex")} are unsupported (little-endian ${String(littleVersion)}, big-endian ${String(bigVersion)}; byte order unknown); supported versions are 2 and 3`,
|
||||
);
|
||||
}
|
||||
if (!this.#little && version !== 3)
|
||||
rejectModel("Big-endian GGUF requires version 3");
|
||||
const tensorCount = await this.#unsigned(8);
|
||||
|
||||
@@ -22,6 +22,57 @@ const providerAt = async (bytes: Buffer) => {
|
||||
return { provider, path, root };
|
||||
};
|
||||
|
||||
it.each([
|
||||
["01000000", 1, 16_777_216],
|
||||
["00000001", 16_777_216, 1],
|
||||
["04000000", 4, 67_108_864],
|
||||
["00000004", 67_108_864, 4],
|
||||
["e7030000", 999, 3_875_733_504],
|
||||
["000003e7", 3_875_733_504, 999],
|
||||
["00000000", 0, 0],
|
||||
["ffffffff", 4_294_967_295, 4_294_967_295],
|
||||
] as const)(
|
||||
"preserves unsupported GGUF version bytes %s without assuming byte order",
|
||||
async (versionBytes, little, big) => {
|
||||
// No counts are needed to explain a version refusal. Do not reinterpret
|
||||
// an unknown version's subsequent layout or publish a header observation.
|
||||
const { provider, path } = await providerAt(
|
||||
Buffer.from(`47475546${versionBytes}`, "hex"),
|
||||
);
|
||||
const result = await provider.inspect({ path, hash: "header" });
|
||||
if (result.ok) throw new Error("Unknown version admitted");
|
||||
expect(result.error).toMatchObject({
|
||||
_tag: "AnalysisUnsupportedTargetError",
|
||||
reason: `GGUF version bytes ${versionBytes} are unsupported (little-endian ${String(little)}, big-endian ${String(big)}; byte order unknown); supported versions are 2 and 3`,
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("continues reading little-endian GGUF v2 declarations", async () => {
|
||||
const { provider, path } = await providerAt(
|
||||
ggufFixture({ version: 2 }).bytes,
|
||||
);
|
||||
const result = await provider.inspect({ path, hash: "header" });
|
||||
if (!result.ok) throw result.error;
|
||||
expect(result.value).toMatchObject({ version: 2, byte_order: "little" });
|
||||
expect(result.value.tensors.map(({ name }) => name)).toEqual([
|
||||
"weight",
|
||||
"quant",
|
||||
]);
|
||||
});
|
||||
|
||||
it("continues refusing the unsupported big-endian GGUF v2 layout", async () => {
|
||||
const { provider, path } = await providerAt(
|
||||
ggufFixture({ version: 2, bigEndian: true }).bytes,
|
||||
);
|
||||
const result = await provider.inspect({ path, hash: "header" });
|
||||
if (result.ok) throw new Error("Big-endian GGUF v2 admitted");
|
||||
expect(result.error).toMatchObject({
|
||||
_tag: "BinaryTargetError",
|
||||
constraint: "file_format_rejected",
|
||||
});
|
||||
});
|
||||
|
||||
it.each([false, true])(
|
||||
"reads exact GGUF declarations, endian=%s, without hashing tensor payload",
|
||||
async (bigEndian) => {
|
||||
|
||||
Reference in New Issue
Block a user