-
Notifications
You must be signed in to change notification settings - Fork 780
fix(catalog): ingest llama.cpp multimodal and dual-envelope metadata #1807
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
5abf553
ad1a895
a81a39f
a88b781
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,16 +3,17 @@ import { catalogHintsFromModelsApiItem } from "../src/codex/catalog/provider-fet | |
| import { extractProviderModelItems } from "../src/providers/model-discovery"; | ||
|
|
||
| /** | ||
| * Regression coverage for the context half of #1797. | ||
| * Regression coverage for #1797 (llama.cpp dual-envelope metadata). | ||
| * | ||
| * A llama.cpp server reports its served context under `meta.n_ctx`, which was in | ||
| * none of the recognized context fields, so a correct local server produced no | ||
| * context evidence at all. | ||
| * A llama.cpp server splits one model across two arrays: an Ollama-style | ||
| * `models[]` carrying `capabilities` and an OpenAI-style `data[]` carrying | ||
| * `meta`. Discovery reads only `data[]`, so the served context (`meta.n_ctx`) | ||
| * and the image signal (`multimodal`) never met and a correct server produced | ||
| * a context-unknown, image-blind row. | ||
| * | ||
| * The image half of #1797 is NOT fixed here and is characterized below: the | ||
| * `multimodal` token lives in the Ollama-style `models[]` array while discovery | ||
| * deliberately reads only `data[]`, and even a merged item would stay | ||
| * image-unknown because `multimodal` is not a recognized capability string. | ||
| * Both halves now resolve. The tests below also pin the boundary that makes | ||
| * the join safe: admission is decided on the original `data[]` row before any | ||
| * enrichment, and only capability keys are copied. | ||
| */ | ||
|
|
||
| const VERBATIM_LLAMACPP_BODY = { | ||
|
|
@@ -62,9 +63,10 @@ describe("llama.cpp served context ingestion (#1797)", () => { | |
| expect(hints.contextWindow).toBe(32768); | ||
| }); | ||
|
|
||
| test("the dual-envelope body yields context but still no image evidence", () => { | ||
| // Characterization of the KNOWN remaining gap in #1797, so the follow-up fix | ||
| // has a live witness and a test to flip rather than a prose claim. | ||
| test("the dual-envelope body now yields BOTH context and image evidence", () => { | ||
| // Was a characterization of the #1797 gap: the multimodal token lived in | ||
| // models[] while discovery read only data[], so the row stayed image-blind. | ||
| // Both halves are now joined on exact id, and multimodal maps to image. | ||
| const extracted = extractProviderModelItems(VERBATIM_LLAMACPP_BODY, { | ||
| maxModels: 100, | ||
| } as never); | ||
|
|
@@ -74,7 +76,67 @@ describe("llama.cpp served context ingestion (#1797)", () => { | |
|
|
||
| const hints = catalogHintsFromModelsApiItem("lidge", items[0] as never); | ||
| expect(hints.contextWindow).toBe(262144); | ||
| // The "multimodal" token was discarded with models[]; unknown, never false. | ||
| expect(hints.inputModalities).toBeUndefined(); | ||
| expect(hints.inputModalities).toEqual(["text", "image"]); | ||
| }); | ||
|
|
||
| test("a data[] value is never overridden by its sibling", () => { | ||
| // models[] is exactly the key discovery refuses to trust as a source of | ||
| // models. Enrichment fills only ABSENT keys, so an authoritative data[] | ||
| // entry always wins. | ||
| const extracted = extractProviderModelItems({ | ||
| models: [{ id: "m", context_length: 999 }], | ||
| data: [{ id: "m", context_length: 111 }], | ||
| }, { maxModels: 100 } as never); | ||
| const items = (extracted as { ok: true; items: Array<Record<string, unknown>> }).items; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win Remove the duplicate Each listed test callback declares Keep one declaration at each location. The declarations in different Also applies to: 101-101, 110-110, 127-127, 139-139 🤖 Prompt for AI AgentsSource: Learnings |
||
| expect(items[0]!.context_length).toBe(111); | ||
| }); | ||
|
|
||
| test("a model present only in the sibling array is still ignored", () => { | ||
| // Membership is decided entirely by data[]; the conservative boundary that | ||
| // refuses a stray models key is preserved. | ||
| const extracted = extractProviderModelItems({ | ||
| models: [{ id: "ghost" }], | ||
| data: [{ id: "real" }], | ||
| }, { maxModels: 100 } as never); | ||
| const items = (extracted as { ok: true; items: Array<Record<string, unknown>> }).items; | ||
| expect(items.map(i => i.id)).toEqual(["real"]); | ||
| }); | ||
|
|
||
| test("an ambiguous sibling id is skipped rather than guessed", () => { | ||
| const extracted = extractProviderModelItems({ | ||
| models: [{ id: "m", context_length: 999 }, { id: "m", context_length: 555 }], | ||
| data: [{ id: "m" }], | ||
| }, { maxModels: 100 } as never); | ||
| const items = (extracted as { ok: true; items: Array<Record<string, unknown>> }).items; | ||
| expect(items[0]!.context_length).toBeUndefined(); | ||
| }); | ||
| test("sibling metadata cannot admit a model the provider filter rejects", () => { | ||
| // Enrichment used to run BEFORE admission filtering, so a models[] entry | ||
| // could supply the exact field a filter required. Reproduced against the | ||
| // real Chutes policy: a row without supported_features:["tools"] was | ||
| // admitted once a same-id sibling provided it. Enrichment may change what | ||
| // is KNOWN about a model, never WHICH models are published. | ||
| const extracted = extractProviderModelItems({ | ||
| models: [{ id: "not-proven-tool-capable", supported_features: ["tools"] }], | ||
| data: [{ id: "not-proven-tool-capable" }], | ||
| }, { | ||
| maxModels: 100, | ||
| spec: { filter: { allOf: [{ path: ["supported_features"], containsAny: ["tools"] }] } }, | ||
| } as never); | ||
|
|
||
| const items = (extracted as { ok: true; items: Array<Record<string, unknown>> }).items; | ||
| expect(items).toEqual([]); | ||
| }); | ||
|
|
||
| test("only capability keys are enriched, not arbitrary fields", () => { | ||
| // A blanket fill-every-absent-key made the untrusted models[] array a way | ||
| // into any field the pipeline consumes. | ||
| const extracted = extractProviderModelItems({ | ||
| models: [{ id: "m", context_length: 999, owned_by: "hostile" }], | ||
| data: [{ id: "m" }], | ||
| }, { maxModels: 100 } as never); | ||
|
|
||
| const items = (extracted as { ok: true; items: Array<Record<string, unknown>> }).items; | ||
| expect(items[0]).toEqual({ id: "m" }); | ||
| }); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win
Require one unambiguous raw sibling identifier before enrichment.
Line 376 indexes every
id,model, andnamevalue as an independent identity. A sibling such as{ id: "model-a", name: "model-b", capabilities: ["multimodal"] }can therefore enrichdata[]row"model-b", although the sibling ID is"model-a".Line 446 also looks up
finalIdafterstripIdPrefixprocessing. A rawdata[]ID of"prefix/model-a"can therefore match a sibling raw ID of"model-a". This violates the required exact same-ID boundary and can publish incorrect capability metadata for an admitted model.Collect the non-empty sibling identity fields first. Skip the sibling when they disagree. Match enrichment against the original
data[]ID, notfinalId. Add regression tests for conflicting sibling aliases and prefix-stripped IDs.Proposed direction
function buildSiblingIndex(value: unknown, limit: number): SiblingIndex | null { // ... for (const raw of sibling) { const entry = plainObject(raw); if (!entry) continue; - for (const key of ["id", "model", "name"]) { - const id = entry[key]; - if (typeof id !== "string" || id.length === 0) continue; - byId.set(id, byId.has(id) && byId.get(id) !== entry ? null : entry); - } + const ids = new Set( + ["id", "model", "name"] + .map(key => entry[key]) + .filter((id): id is string => typeof id === "string" && id.length > 0), + ); + if (ids.size !== 1) continue; + const [id] = ids; + byId.set(id, byId.has(id) && byId.get(id) !== entry ? null : entry); } } -function enrichAdmittedModel(item: ProviderModelsApiItem, siblings: SiblingIndex): ProviderModelsApiItem { - const extra = siblings.get(item.id); +function enrichAdmittedModel( + item: ProviderModelsApiItem, + rawId: string, + siblings: SiblingIndex, +): ProviderModelsApiItem { + const extra = siblings.get(rawId); // ... } - items.push(siblings ? enrichAdmittedModel(item, siblings) : item); + items.push(siblings ? enrichAdmittedModel(item, id, siblings) : item);Also applies to: 376-382, 387-396, 404-420, 438-446
🤖 Prompt for AI Agents
Source: Path instructions