Repository navigation
fix(usai-catalog): refuse a near-miss instead of enriching from the wrong model - #484
Conversation
…rong model
The models.dev enrichment matcher scored candidates but never rejected one, so
a gateway model absent from the enrichment source was enriched with the nearest
same-family model's data. With only `claude-opus-5` / `claude-sonnet-5` present
under the Anthropic provider, `claude_4_5_haiku` came out with Opus numbers:
context 1,000,000 and $5/$25 input/output, where the reviewed values are
200,000 and $1/$5. A 5x price overstatement, presented as fact.
Mis-enrichment is worse than missing data. The fields come out populated and
plausible, so a completeness check ("every model has a price") passes while the
values are wrong.
Scoring alone cannot fix this: it ranks whatever field it is handed, and the
best of a bad field still wins. Invented ids that exist nowhere in the source
(`claude_9_9_nonexistent`, `llama_99_fake`) all matched something.
Add an eligibility guard ahead of the score. A candidate must agree on family,
tier (haiku is not opus, flash is not pro), major.minor version (4.5 is not 5)
and the `lite` qualifier, or it is not considered at all. No match now means the
model is emitted visibly unenriched rather than silently mis-enriched.
The scoring terms the guard subsumes are removed; what remains discriminates
only between duplicate listings of the SAME model (dated SKUs, regional
prefixes, modality siblings). `lite` detection moves from a substring test to a
token test. The verified normalizations are unchanged: gateway deployment
suffixes still strip (`gpt_5_5_default_v2` matches `gpt-5.5`) and the
`llama4`->`llama-4` glue-split still matches the Bedrock form.
A model that could not be enriched is now reported by id on stderr, with the
fallback limits named, so "could not look it up" is distinguishable from
"looked it up" instead of both rendering as a complete-looking entry.
Adds 23 hermetic tests with inline fixtures, so a refresh of the committed
models-dev subset cannot quietly turn a negative case positive. Proved failable:
neutering the guard fails 11 of them. The committed catalog.json is unchanged
(bootstrap output is byte-identical), and the byte-exact round-trip guards for
both shipped opencode.jsonc copies still pass.
Verified: 63/63 providers/usai, 59/59 usai-provider kit, 13/13 paseo,
make validate and make validate-kits clean.
Co-authored-by: OpenCode Agent <[email protected]>
Reject base/codex/max near-matches before scoring and report unmatched pricing separately from actual feed-limit fallbacks. Co-authored-by: OpenCode Agent <[email protected]>
…sion-guard # Conflicts: # integrations/providers/usai/tests/feed-fixture.test.mjs
basiliskus
left a comment
There was a problem hiding this comment.
Verdict: request changes — e1346ad
The precision guard still treats unknown same-version qualifiers as the base model. isPreciseMatch compares only the hard-coded codex, mini, nano, and max tokens, so I reproduced gpt-5.2-luna -> gpt-5.2, claude-opus-5-thinking -> claude-opus-5, and gemini-2.5-flash-preview -> gemini-2.5-flash. A future qualified gateway ID can therefore receive a sibling's limits and pricing, recreating the defect this PR is meant to close. Please make identity comparison fail closed for unrecognized tokens (while explicitly normalizing known provider/region/date/deployment noise) and add a regression test for an unknown same-version qualifier.
Assisted-by: OpenCode [gpt-5.6-sol-latest-guardrails-defaultv2] skill:change-review
The precision guard compared a hard-coded qualifier list (`codex`,
`mini`, `nano`, `max`), so any qualifier outside that list was invisible
to it and a qualified gateway id collapsed onto its base model,
inheriting that sibling's limits and pricing.
Reproduced against the guard as it stood:
gpt-5.2-luna vs gpt-5.2 -> MATCH
claude-opus-5-thinking vs claude-opus-5 -> MATCH
gemini-2.5-flash-preview vs gemini-2.5-flash -> MATCH
That is the same mis-enrichment defect this guard exists to prevent, for
every qualifier the list did not happen to name.
Enumerating qualifiers cannot close this: the set is open and owned by
the vendors, so the list is always one release behind. Invert it instead.
`identityResidue` collects every token that is NOT a family, variant,
version, or known packaging token, and a candidate must agree on that
whole remainder. Unknown input therefore fails closed by construction
rather than by enumeration.
Enumerating NOISE is safe in a way enumerating qualifiers is not: the
noise set is small, observable in the catalogs actually read (provider
prefixes, region scopes, marketplace revisions, parameter counts, tuning
descriptors), and being wrong about it costs only a missed enrichment,
which is already reported as unenriched rather than silently wrong.
FAMILY_TOKEN and VARIANT_TOKEN are hoisted so the tokenizer and the
residue cannot drift apart — a token consumed as a family or variant must
not also count as identity-bearing remainder.
Adds five regression tests using deliberately INVENTED qualifiers
(`luna`, `thinking`, `preview`, `solstice`). A test using a real
qualifier would start passing the moment someone adds that token to a
list, which is the failure mode being guarded against; an invented token
can only pass if identity comparison is genuinely closed over unknown
input. They cover both directions (qualified->base and base->qualified),
two different unknown qualifiers against each other, and that failing
closed does not mean failing always: a qualified id still matches its own
exact entry.
Verified the new tests fail against the previous guard (5 failed, 24
passed) and pass with this change (29 passed), so they are regression
tests and not restatements. Full providers/usai suite 86 passed across 8
files, `build-catalog.mjs --check` reports catalog.json up to date (20
models), and enrichment of the committed fixtures is unchanged — the same
4 models were already unenriched before this change, so no real model
loses data.
Co-authored-by: OpenCode Agent <[email protected]>
|
AI-assisted follow-up (OpenCode), advisory — needs a human to confirm before it drives a change. Pushed @basiliskus's finding was correct, and I reproduced it before changing anything. Extracted the guard from this PR's All three reported false matches reproduce. The controls confirm the guard was not broken generally — it failed open only for tokens absent from its hardcoded list, which is precisely the diagnosis in the review. The fix inverts the comparison instead of extending the listI did not add
function identityResidue(normalizedId) {
const residue = new Set()
for (const part of normalizedId.split(/[.\-/:]/)) {
if (!part) continue
if (FAMILY_TOKEN.test(part) || VARIANT_TOKEN.test(part)) continue
if (/^\d+$/.test(part)) continue
if (IDENTITY_NOISE.some((re) => re.test(part))) continue
residue.add(part)
}
return residue
}Unknown input now fails closed by construction rather than by enumeration. Why enumerating noise is safe where enumerating qualifiers was not. The noise set is small, observable in the catalogs actually read (provider prefixes, region scopes, marketplace revisions like I also hoisted Tests use invented qualifiers on purposeFive new tests, with deliberately fictional tokens ( A test using a real qualifier would start passing the moment someone adds that token to a list — which is the failure mode being guarded against. An invented token can only pass if identity comparison is genuinely closed over unknown input. The review asked for coverage of "an unknown same-version qualifier"; I added the reverse direction too, since a request for the base model is the more common case and a base→qualified match is equally damaging. And one positive control, because failing closed must not mean failing always: a qualified id still matches its own exact entry. VerificationConfirmed these are regression tests, not restatements — the new tests against the previous guard: With the fix, in a network-off container: No real model loses data. I ran One thing I want to flag myself
Also: |
|
Branch updated to current The merge was clean. Worth noting why that wasn't a given: Re-ran the full suite after merging rather than assuming it still held: No code changed in this push — it is a merge commit only. The fix itself is still @basiliskus this is ready for another look when you have time. One process note: the repository ruleset has AI-assisted (OpenCode), advisory — a human should confirm before this merges. |
basiliskus
left a comment
There was a problem hiding this comment.
Verdict: request changes — 2a0816a
The prior unknown-qualifier issue is resolved, including class-level regression coverage. One over-refusal remains at integrations/providers/usai/scripts/build-catalog.mjs:253-299,318-339: known date and regional packaging noise can now prevent enrichment. I reproduced gpt-4o -> gpt-4o-2024-08-06 and claude-opus-5 -> eu.anthropic.claude-opus-5 returning no match, while the comparable provider, US-region, Anthropic-date, and gateway-deployment cases still match. Please preserve these legitimate normalizations without weakening the fail-closed behavior for unknown identity tokens, and add positive controls for both forms.
Assisted-by: OpenCode [gpt-5.6-sol-latest-guardrails-defaultv2] skill:change-review
Model valid terminal release dates separately from semantic major/minor versions. Dated gateway IDs now require the same snapshot, while undated aliases prefer an undated entry or deterministically choose the latest valid dated snapshot. Treat provider/region names as packaging only in leading prefix positions and marketplace revisions only in the terminal position. Keep malformed dates, extra numeric qualifiers, duplicate structural tokens, modality markers, and unknown qualifiers in ordered identity residue so near-misses fail closed. Compare numeric identity as strings to avoid Number precision collapse. Add positive, negative, calendar, insertion-order, provider, Bedrock, Cohere, and precision regressions. Co-authored-by: OpenCode Agent <[email protected]>
|
AI-assisted follow-up (OpenCode). Pushed The matcher now:
Verification at the committed head: Please re-review exact head |
basiliskus
left a comment
There was a problem hiding this comment.
Verdict: approve — 933169d
The prior date/region over-refusal is resolved. I reproduced both formerly failing matches (gpt-4o to a dated alias and claude-opus-5 to a sole EU-prefixed entry), and the added controls preserve those normalizations while retaining exact-date and unknown-qualifier rejection. I found no new issues in the delta since 2a0816a6.
Assisted-by: OpenCode [gpt-5.6-sol-latest-guardrails-defaultv2] skill:change-review
🤖 I have created a release *beep* *boop* --- ## [1.15.0](v1.14.0...v1.15.0) (2026-10-21) ### Features * **acq-kits:** add OpenDesign browser kit ([#438](#438)) ([83e202f](83e202f)) * **isolation:** define provider-neutral input contract ([#518](#518)) ([28a8ebd](28a8ebd)) * **usai-catalog:** refresh models and withhold undeployed gateway entries ([#519](#519)) ([03421e0](03421e0)) ### Bug Fixes * **usai-catalog:** refuse a near-miss instead of enriching from the wrong model ([#484](#484)) ([c3a6dac](c3a6dac)) --- This PR was generated with [Release Please](https://github.com/googleapis/release-please). See [documentation](https://github.com/googleapis/release-please#release-please). Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
## Summary Moves `PATTERNS_KIT_REF` from v1.12.0 (`56d1f5f`) to agentic-coding-patterns v1.15.0 (`f3c6acb`), so the built-in kit bundle picks up: - **opencode kit → OpenCode 2.0.30** (GSA-TTS/agentic-coding-patterns#499). OpenCode 2.0.0–2.0.29 ignore config `instructions` (anomalyco/opencode#51341, fixed in 2.0.30), so a sandbox on an image without a baked OpenCode (`devenv-shell`, or the `devenv-shell` default proposed in #584) currently runs the bundled 2.0.22 and silently drops `instructions`-based conventions. - **paseo kit → Paseo 0.12.0** (GSA-TTS/agentic-coding-patterns#504): fixes for OpenCode v2 under Paseo (daemon OOM on long v2 streams, multi-select answers, HTTP 500s instead of tool errors) and the host-sleep daemon-status fix. - **Kit verify scripts gain a "could not check" verdict** (GSA-TTS/agentic-coding-patterns#460); it only affects kit verify output. Also v1.13.0: usai-provider's host-side catalog normalizer (#436), the paseo identity-gate script (#453), and the team-kit and personal-kit templates under `acq-kits/examples/`. None changes what acq applies by default beyond the two kits above. Also v1.15.0: refreshed usai-provider model catalog (GSA-TTS/agentic-coding-patterns#519, GSA-TTS/agentic-coding-patterns#484), the OpenDesign opt-in kit (GSA-TTS/agentic-coding-patterns#438), and the provider-neutral input contract (GSA-TTS/agentic-coding-patterns#518). acq's provider-facts loader reads `provider-facts/usai.env`, which v1.15.0 does not change. The doc examples and Bats assertions that spell the pin move with it. KNOWN_FAILURE_MODES §9 now names 2.0.30 as the version the kit installs and notes its 15-minute default `chunkTimeout` (the timeout message and retry limits are unchanged from 2.0.22). ## Verification On the v1.15.0 pin: - Focused Bats: `135-configure`, `95-kit-ref-persistence`, `36-agent-catalog`, `50-kit-list-completeness` (63/63) - markdownlint on the changed docs On the earlier v1.14.0 pin (not re-run for v1.15.0): - `npm run lint:md` - `RUN_ACQ=1 scripts/verify-backends --only msb` on macOS, msb 0.7.8: 16 passed, 1 skipped (playbook fetch, no `gh` login), run with `TMPDIR` set to its resolved `/private/var/...` path to get past an unrelated create failure that also hits `main` (#594) - Downstream, with this pin on msb 0.7.8 and `ACQ_IMAGE=devenv-shell:1.0.0`: a team kit's end-to-end verify passed 71/71 (OpenCode 2.0.30 runs and loads `instructions`); with the paseo kit added, Paseo 0.12.0 drove an OpenCode 2.0.30 agent that received the team `instructions`, and its sessions survived a backup, `acq rm`, recreate and restore ## Rollback Revert this PR to restore the v1.12.0 bundle.
Fixes #387.
The defect
integrations/providers/usai/scripts/build-catalog.mjs's models.dev matcher scored candidates but never rejected one. A gateway model absent from the enrichment source was enriched with the nearest same-family model's data.Reproduced offline against the committed fixtures (
--models-url tests/fixtures/models-list.json --models-dev tests/fixtures/models-dev.json):A 5x price overstatement, presented as fact. The fixture contains no haiku entry at all.
Scoring alone cannot fix this — it ranks whatever field it is handed, and the best of a bad field still wins. There was no floor, so ids that exist nowhere in the source still matched:
Mechanism: a variant match scored
+50, but a variant mismatch scored nothing. Onlylitecarried a mismatch penalty. Base score10survived every disagreement.This is worse than missing data: the fields come out populated and plausible, so a completeness check ("every model has a price") passes while the values are wrong.
The fix
An eligibility guard ahead of the score (
isPreciseMatch). A candidate must agree on all of:liteNo match now means the model is emitted visibly unenriched rather than silently mis-enriched.
The scoring terms the guard subsumes are removed — a hard tier check plus a
-20penalty forprowould be two mechanisms for one rule. What remains discriminates only between duplicate listings of the same model: dated SKUs,us./eu.regional prefixes, modality siblings (-tts,-vision).litedetection moves from a substring test to a token test.The verified normalizations are unchanged: gateway deployment suffixes still strip (
gpt_5_5_default_v2matchesgpt-5.5) and thellama4->llama-4glue-split still matches the Bedrock form.Unenrichment is reported, not silent
The issue asked for absent fields. Limits cannot be absent here —
shapeFromFeedsappliesFALLBACK_LIMITS(128000/8192), and making them absent would make the opencode emitter write literalundefinedinto the JSON (a separate defect, not fixed in this PR). Cost has no fallback, so it genuinely goes absent.So the fallback is made loud instead:
Suppressed under
--no-enrichment, so "did not look" cannot masquerade as "looked and failed".Note
claude_4_7_opus/claude_4_8_opus: these previously matchedclaude-opus-5— a different model — and their numbers merely coincided with the reviewed values. The fixture has no 4.7/4.8 entry. They were right by luck, and now report honestly.Tests
23 new hermetic tests in
tests/matcher-precision.test.mjs, covering the four negative classes the issue names plus the positive controls it requires.Fixtures are inline, not the committed
models-dev.jsonsubset — a future refresh of that subset must not be able to quietly turn a negative case positive.Proved failable: neutering the guard with an early
return truefails 11 of 23, while all 12 positive controls stay green. The tests exercise the guard rather than merely passing alongside it.Verification
catalog.jsonis unchanged — it is bootstrapped from the shippedopencode.jsonc, and--bootstrapoutput is byte-identical to the committed file. The byte-exact round-trip guards for both shippedopencode.jsonccopies still pass.This is therefore a latent fix: it changes nothing shipped today, and makes the next live regeneration trustworthy. That is the condition the issue describes as the reason the guard exists.
AI-assisted (OpenCode). Advisory — requires human review and sign-off.