Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: CopilotKit/aimock
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: main@{1day}
Choose a base ref
...
head repository: CopilotKit/aimock
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: main
Choose a head ref
  • 7 commits
  • 6 files changed
  • 3 contributors

Commits on Jul 24, 2026

  1. Configuration menu
    Copy the full SHA
    85fc683 View commit details
    Browse the repository at this point in the history
  2. Configuration menu
    Copy the full SHA
    1f3fa01 View commit details
    Browse the repository at this point in the history
  3. chore(deps): update github actions (#338)

    This PR contains the following updates:
    
    | Package | Type | Update | Change |
    |---|---|---|---|
    | [docker/login-action](https://redirect.github.com/docker/login-action)
    | action | minor | `v4.4.0` → `v4.5.1` |
    |
    [zizmorcore/zizmor-action](https://redirect.github.com/zizmorcore/zizmor-action)
    | action | patch | `v0.6.0` → `v0.6.1` |
    
    ---
    
    ### Release Notes
    
    <details>
    <summary>docker/login-action (docker/login-action)</summary>
    
    ###
    [`v4.5.1`](https://redirect.github.com/docker/login-action/compare/v4.5.0...v4.5.1)
    
    [Compare
    Source](https://redirect.github.com/docker/login-action/compare/v4.5.0...v4.5.1)
    
    ###
    [`v4.5.0`](https://redirect.github.com/docker/login-action/compare/v4.4.0...v4.5.0)
    
    [Compare
    Source](https://redirect.github.com/docker/login-action/compare/v4.4.0...v4.5.0)
    
    </details>
    
    <details>
    <summary>zizmorcore/zizmor-action (zizmorcore/zizmor-action)</summary>
    
    ###
    [`v0.6.1`](https://redirect.github.com/zizmorcore/zizmor-action/releases/tag/v0.6.1)
    
    [Compare
    Source](https://redirect.github.com/zizmorcore/zizmor-action/compare/v0.6.0...v0.6.1)
    
    zizmor 1.28.0 is now the default version used by the action.
    
    </details>
    
    ---
    
    ### Configuration
    
    📅 **Schedule**: (in timezone America/Los_Angeles)
    
    - Branch creation
      - "before 9am every weekday"
    - Automerge
      - At any time (no schedule defined)
    
    🚦 **Automerge**: Enabled.
    
    ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the
    rebase/retry checkbox.
    
    👻 **Immortal**: This PR will be recreated if closed unmerged. Get
    [config
    help](https://redirect.github.com/renovatebot/renovate/discussions) if
    that's undesired.
    
    ---
    
    - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check
    this box
    
    ---
    
    This PR was generated by [Mend Renovate](https://mend.io/renovate/).
    View the [repository job
    log](https://developer.mend.io/github/CopilotKit/aimock).
    
    <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yNzUuMiIsInVwZGF0ZWRJblZlciI6IjQzLjI3NS4yIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
    renovate[bot] authored Jul 24, 2026
    Configuration menu
    Copy the full SHA
    bdbd48d View commit details
    Browse the repository at this point in the history
  4. fix(drift): classify gemini-1.5-pro/flash in excludeFamilies (retired…

    … upstream, still mocked)
    
    Both families are gone from Gemini's live /models listing but are still
    legitimately referenced in aimock source, so the deprecation canary kept
    routing them to needs-human every run (see the two notes under
    drift-proposals/). The human decision is: keep mocking them.
    
    Move them out of includeFamilies.gemini into excludeFamilies.gemini's
    "Retired / legacy specialty" bucket, the established pattern for a
    retired-but-still-mocked family (gemini-pro, gpt-3.5, gemini-2.0-flash-exp;
    prior art in #315). excludeFamilies means "not counted as text-generation
    drift", not "not mocked" — every fixture, capability test and the
    isReasoningModel() === false verdict for gemini-1.5-* is untouched.
    
    Re-pin the two affected DATA_FROZEN membership hashes accordingly.
    
    Side effect: gemini's truncated-listing trust floor drops from 11 to 9
    (floor = includeFamilies[provider].size).
    jpr5 committed Jul 24, 2026
    Configuration menu
    Copy the full SHA
    a3dc250 View commit details
    Browse the repository at this point in the history
  5. chore(drift-sync): needs-human model-family decision (2026-07-24) (#337)

    ## Needs a human decision (drift-sync)
    
    The deterministic, zero-LLM drift-sync found a model-family change it
    must
    NOT auto-apply (a genuinely new/unclassified family, a still-referenced
    deprecation, or a registry structural mismatch). It wrote the note
    file(s)
    below and opened this PR so the decision is REACHABLE in the repo.
    
    > **Not auto-merged — a human decides.**
    >
    > ⚠️ **The `Decision: include` instructions in this template do NOT
    apply to this
    > PR.** That protocol is scoped to a *new-family* note only:
    `renderProposalNote`
    > emits the `## Decision` block solely under `kind === "new-family"`,
    and
    > `parseProposalDecision` has exactly one call site — the addition half
    of
    > `runDriftSyncCore`. The deprecation half never reads a decision at
    all.
    >
    > Both notes in this PR are **still-referenced deprecations**, and they
    contain
    > **no `Decision:` line whatsoever**. Writing one would be a no-op.
    There is no
    > marker, of any value, that makes a later run mechanically resolve this
    note
    > class — only a human registry edit can. **That edit is now part of
    this PR**
    > (see *Human decision* at the bottom), so there is no two-run hand-off
    here:
    > merging this PR both records the decision and resolves the drift.
    >
    > (For reference, the original template text: for a *new-family* note
    you would
    > set `Decision: include` to classify it, or delete the note / close the
    PR to
    > reject, then merge — and the next run would apply the registry edit
    itself.)
    
    ### Note file(s) in this PR
    
    <!-- drift-changeset: e08d8fcc953752d5 -->
    - `drift-proposals/gemini-gemini-1.5-flash-deprecated-referenced.md`
    <!-- drift-proposal-note:
    drift-proposals/gemini-gemini-1.5-flash-deprecated-referenced.md -->
    - `drift-proposals/gemini-gemini-1.5-pro-deprecated-referenced.md`
    <!-- drift-proposal-note:
    drift-proposals/gemini-gemini-1.5-pro-deprecated-referenced.md -->
    
    ### drift-sync outcome
    
    ```
    npm warn Unknown project config "minimum-release-age". This will stop working in the next major version of npm. See `npm help npmrc` for supported config options.
    npm warn Unknown project config "block-exotic-subdeps". This will stop working in the next major version of npm. See `npm help npmrc` for supported config options.
    needs-human note(s) written this run — routed to human without a live re-collect (no registry edit to re-verify)
      [needs-human-still-referenced] gemini/gemini-1.5-flash: "gemini-1.5-flash" is deprecated but still referenced in source — routed to human (drift-proposals/gemini-gemini-1.5-flash-deprecated-referenced.md)
      [needs-human-still-referenced] gemini/gemini-1.5-pro: "gemini-1.5-pro" is deprecated but still referenced in source — routed to human (drift-proposals/gemini-gemini-1.5-pro-deprecated-referenced.md)
      [skipped] anthropic: live /models listing too short to trust for anthropic (10 raw id(s), need >= 19 — the number of families aimock mocks for this provider) — never mass-removing off a truncated or empty listing
    npm warn Unknown env config "block-exotic-subdeps". This will stop working in the next major version of npm. See `npm help npmrc` for supported config options.
    npm warn Unknown env config "minimum-release-age". This will stop working in the next major version of npm. See `npm help npmrc` for supported config options.
    npm warn Unknown project config "minimum-release-age". This will stop working in the next major version of npm. See `npm help npmrc` for supported config options.
    npm warn Unknown project config "block-exotic-subdeps". This will stop working in the next major version of npm. See `npm help npmrc` for supported config options.
    ^[[33m[STARTED]^[[39m Backing up original state...
    ^[[32m[COMPLETED]^[[39m Backed up original state in git stash (c62c488)
    ^[[33m[STARTED]^[[39m Running tasks for staged files...
    ^[[33m[STARTED]^[[39m package.json^[[0;90m — 2 files^[[0m
    ^[[33m[STARTED]^[[39m *.{ts,mts,js,mjs,cjs,json,html,css,md}^[[0;90m — 2 files^[[0m
    ^[[33m[STARTED]^[[39m *.{ts,mts,js,mjs}^[[0;90m — 0 files^[[0m
    ^[[33m[SKIPPED]^[[39m *.{ts,mts,js,mjs}^[[0;90m — no files^[[0m
    ^[[33m[STARTED]^[[39m prettier --write
    ^[[32m[COMPLETED]^[[39m prettier --write
    ^[[32m[COMPLETED]^[[39m *.{ts,mts,js,mjs,cjs,json,html,css,md}^[[0;90m — 2 files^[[0m
    ^[[32m[COMPLETED]^[[39m package.json^[[0;90m — 2 files^[[0m
    ^[[32m[COMPLETED]^[[39m Running tasks for staged files...
    ^[[33m[STARTED]^[[39m Applying modifications from tasks...
    ^[[32m[COMPLETED]^[[39m Applying modifications from tasks...
    ^[[33m[STARTED]^[[39m Cleaning up temporary files...
    ^[[32m[COMPLETED]^[[39m Cleaning up temporary files...
    [fix/drift-2026-07-24-30074183288 85fc683] fix(drift-sync): mechanical model-family sync (needs-human note file(s))
     2 files changed, 14 insertions(+)
     create mode 100644 drift-proposals/gemini-gemini-1.5-flash-deprecated-referenced.md
     create mode 100644 drift-proposals/gemini-gemini-1.5-pro-deprecated-referenced.md
    reason=needs-human
    changeset-key=e08d8fcc953752d5
    ```
    
    ---
    
    ## Human decision: keep mocking these families
    
    **Decision: KEEP.** `gemini-1.5-pro` and `gemini-1.5-flash` are gone
    from Gemini's
    live `/models` listing, but aimock still mocks them on purpose — clients
    pinned to
    older SDKs and older CopilotKit configs still send those ids, and
    `isReasoningModel()` must keep answering `false` for them.
    
    So nothing is being removed. Instead, commit `a3dc250` records the
    decision in the
    registry by moving both families from `includeFamilies.gemini` into
    `excludeFamilies.gemini`'s existing **"Retired / legacy specialty"**
    bucket, and
    re-pins the two affected `DATA_FROZEN` membership hashes.
    
    `excludeFamilies` means *"not counted as text-generation **drift**"*,
    not
    *"not mocked"* — `gemini-pro`, `gpt-3.5` and `gemini-2.0-flash-exp`
    already live
    there while being mocked and heavily referenced in `src/`. Prior art for
    exactly
    this move: #315 (`fix(drift): classify bare chat-latest alias in openai
    excludeFamilies`).
    
    Because `detectDeprecatedFamiliesForSync` draws its candidate set from
    `includeFamilies[provider]`, the two families can no longer become
    deprecation
    candidates — drift-sync stops re-flagging them needs-human, which ends
    the daily
    red job and the daily Slack page. The two note files stay in this PR as
    the audit
    trail for the `excludeFamilies` entries; they become inert on merge and
    will never
    be re-read or re-written.
    
    **Nothing that references `gemini-1.5-*` was touched.**
    `src/model-utils.ts:54`
    (`NONREASONING_FAMILIES`), the three gemini reasoning-capability suites,
    the
    `gemini.test.ts` / `vertex-ai.test.ts` conformance assertions and the
    recorded
    `sdk-shapes.ts` fixture are all unchanged.
    
    ### Diff
    
    ```
     src/__tests__/drift/logic-pin.test.ts |  4 ++--
     src/__tests__/drift/model-registry.ts | 12 +++++++++---
     2 files changed, 11 insertions(+), 5 deletions(-)
    ```
    
    ### Known side effect
    
    Gemini's truncated-listing trust floor is
    `includeFamilies[provider].size`, so it
    drops from 11 to 9. Reversible: move the families back and re-pin.
    
    ---
    
    ## Local red-green proof
    
    ### 1. Baseline — before any edit, drift suites all green
    
    ```
    $ pnpm exec vitest run src/__tests__/drift/
     ✓ src/__tests__/drift/model-registry.ts ... (all 9 files)
     Test Files  9 passed (9)
          Tests  87 passed (87)
    ```
    
    ### 2. RED — registry membership moved, pins NOT yet updated
    
    `logic-pin.test.ts` fails loudly on the stale membership pins, exactly
    as its
    module doc promises:
    
    ```
    $ pnpm exec vitest run src/__tests__/drift/logic-pin.test.ts
    
     × classification-data membership freeze > freezes includeFamilies.gemini membership
       → Frozen data set "includeFamilies.gemini" membership changed (now:
         ["gemini-2.0-flash","gemini-2.0-flash-lite","gemini-2.5-flash",
          "gemini-2.5-flash-lite","gemini-2.5-pro","gemini-3.1-flash-lite",
          "gemini-3.5-flash","gemini-3.5-flash-lite","gemini-3.6-flash"]).
         Expected: "4a9428b64ffcff0fbb79878d88ed993ffac53e43d64e26bcf5d86509626f593d"
         Received: "c2e2c56b8f8d5fc56152b4633e7d3782e95b7eeb9bc123da71f00e884a54a743"
    
     × classification-data membership freeze > freezes excludeFamilies.gemini membership
       → Frozen data set "excludeFamilies.gemini" membership changed (now:
         ["aqa","gemini-1.5-flash","gemini-1.5-pro","gemini-2.0-flash-exp", ...]).
         Expected: "e3545138234ad782937f66760bef942fca7d4bd0934a87da30bf6e5816ba69b1"
         Received: "c95dedab7588212bbba0bf9ab6434bfd43a449adbe7b35f81f48271ee849a9c2"
    
     Test Files  1 failed (1)
          Tests  2 failed | 18 passed (20)
    ```
    
    The two new pin values were derived independently —
    `sha256(JSON.stringify([...set].sort()))`
    recomputed from `model-registry.ts` in a standalone script, matching the
    values the
    test itself reports:
    
    ```
    includeFamilies.gemini c2e2c56b8f8d5fc56152b4633e7d3782e95b7eeb9bc123da71f00e884a54a743
    excludeFamilies.gemini c95dedab7588212bbba0bf9ab6434bfd43a449adbe7b35f81f48271ee849a9c2
    ```
    
    ### 3. GREEN — pins re-pinned, same command
    
    ```
    $ pnpm exec vitest run src/__tests__/drift/logic-pin.test.ts
     ✓ src/__tests__/drift/logic-pin.test.ts (20 tests) 3ms
     Test Files  1 passed (1)
          Tests  20 passed (20)
    ```
    
    ### 4. Full suite — zero regressions
    
    ```
    $ pnpm test
     Test Files  165 passed | 1 skipped (166)
          Tests  4724 passed | 47 skipped (4771)
    ```
    
    And the suites that specifically pin `gemini-1.5-*` behavior:
    
    ```
    $ pnpm exec vitest run src/__tests__/model-utils.test.ts \
        src/__tests__/gemini-reasoning-capability.test.ts \
        src/__tests__/gemini-audio-reasoning-capability.test.ts \
        src/__tests__/gemini-toolonly-reasoning-capability.test.ts \
        src/__tests__/gemini.test.ts src/__tests__/vertex-ai.test.ts \
        src/__tests__/drift/ src/__tests__/drift-sync-core.test.ts
     Test Files  16 passed (16)
          Tests  236 passed (236)
    ```
    
    ### 5. Drift resolution proved offline
    
    `scripts/drift-sync.ts` has no `--dry-run` / `--check` flag, so the
    canary was
    exercised directly instead: `detectDeprecatedFamiliesForSync` is
    exported and
    pure, so it was called against a simulated live `/models` listing
    containing every
    gemini family aimock classifies **except** `gemini-1.5-pro` /
    `gemini-1.5-flash`
    (31 ids — comfortably over the trust floor under both the old size 11
    and the new
    size 9), once on the pre-fix tree and once on the fixed tree.
    
    Pre-fix (this branch at `85fc683`) — reproduces the needs-human routing:
    
    ```
    includeFamilies.gemini.size (trust floor) = 11
    simulated live listing length            = 31
    status = checked
    candidates = [{"provider":"gemini","family":"gemini-1.5-flash","stillReferenced":true},
                  {"provider":"gemini","family":"gemini-1.5-pro","stillReferenced":true}]
    gemini-1.5 candidates    = 2
    needs-human candidates   = 2
    >>> STILL FLAGGED (red)
    ```
    
    Post-fix (`a3dc250`) — resolved:
    
    ```
    includeFamilies.gemini.size (trust floor) = 9
    simulated live listing length            = 31
    status = checked
    candidates = []
    gemini-1.5 candidates    = 0
    needs-human candidates   = 0
    >>> RESOLVED: gemini-1.5 NOT flagged
    ```
    
    `detectDeprecatedFamiliesForSync` is the exact function `drift-sync.ts`
    uses to build
    the deprecation candidate set, so this is the real routing surface, not
    a stand-in.
    
    **Plus a genuine LIVE confirmation from this PR's own CI.** The
    `drift-live-pr` check
    ran the drift suite against the real provider `/models` listings on this
    head commit
    (`preflight: Gemini key OK`) and the uploaded head drift report contains
    exactly one
    entry — a pre-existing *anthropic* `claude-opus-5` unclassified-family
    finding, also
    present in the base report — with **zero** `gemini-1.5` mentions and no
    gemini entry
    at all:
    
    ```
    Drift gate PASSED: 1 advisory (pre-existing) finding(s) surfaced, none new-in-head.
    ```
    
    That confirms against the live listing that moving both families to
    `excludeFamilies` introduced no new drift and no unclassified-family
    regression
    (`isClassifiedFamily` is satisfied by include **or** exclude). To be
    precise about
    what it does *not* prove: this leg does not surface the
    still-referenced-deprecation
    class as a report entry — the base report shows zero `gemini-1.5`
    mentions too — so
    the live job is regression evidence, and the needs-human routing fix
    itself is
    proved by the `detectDeprecatedFamiliesForSync` red-green above.
    
    ### 6. Pre-push gates
    
    ```
    $ pnpm format:check   → All matched files use Prettier code style!
    $ pnpm lint           → clean (no output)
    $ pnpm exec tsc --noEmit → clean (no output)
    $ pnpm build          → ✔ Build complete (273 files)
    $ pnpm test:exports   → No problems found 🌟
    ```
    
    ### 7. Zero behavior change → no version bump, no changeset
    
    The test that matters is not "which directory did the files sit in" but
    **does this
    change aimock's behavior for a consumer?** It does not, and that is
    verified three
    independent ways.
    
    **(a) The registry is not reachable from shipped code.** Every importer
    of
    `src/__tests__/drift/model-registry.ts` is a test or the maintenance
    script:
    
    ```
    scripts/drift-sync.ts                              (repo script, not published)
    src/__tests__/drift-sync-core.test.ts
    src/__tests__/drift-sync-mirror-equivalence.test.ts
    src/__tests__/drift/logic-pin.test.ts
    src/__tests__/drift/model-registry.test.ts
    src/__tests__/drift/models.drift.ts
    ```
    
    No shipped-source importer exists — `src/model-utils.ts`,
    `src/server.ts`, the
    handlers and every published entrypoint in `tsdown.config.ts` are all
    absent from
    that list. `includeFamilies` / `excludeFamilies` are drift-detection
    *metadata*, not
    a runtime allowlist. Confirmed at the artifact level too: `dist/`
    contains zero
    occurrences of `includeFamilies`, `excludeFamilies` or
    `isClassifiedFamily`, so the
    data isn't inlined either, and `npm pack --dry-run` ships 576 files with
    **none**
    matching `__tests__` / `/drift/` / `model-registry` / `logic-pin` /
    `src/`
    (`files` allowlist is
    `["dist","fixtures",".claude-plugin","skills","CHANGELOG.md"]`;
    `scripts/` ships 0 entries; no `.npmignore`).
    
    **(b) The published artifact is byte-identical.** Built `dist/` from the
    pre-fix and
    post-fix trees and compared all 285 shipped code/declaration files. Note
    the build
    is **not** reproducible — two builds of *identical* source differ in
    declaration
    sourcemaps and in tsdown's non-deterministic import-alias numbering
    (`node_http0` ↔ `node_http1`), so a naive whole-dist hash gives a false
    positive.
    Normalizing that noise, with two identical-source builds as the control
    noise floor:
    
    ```
    control: two IDENTICAL-source builds   → files=285 differing=0
    subject: PRE-fix vs POST-fix build     → files=285 differing=0
    VERDICT: PUBLISHED ARTIFACT SEMANTICALLY IDENTICAL
    ```
    
    **(c) Consumer-visible behavior is bit-identical.** Probed the *built*
    `dist/` before
    and after with real requests for both ids — `generateContent`, SSE
    `streamGenerateContent`, strict-on and strict-off reasoning, the
    OpenAI-compatible
    `/v1/chat/completions` surface, and `/v1/models` — plus the
    `isReasoningModel()`
    verdicts and the captured capability warnings. The two JSON captures
    compare **equal**:
    
    ```
    PRE  isReasoningModel: {'gemini-1.5-pro': False, 'gemini-1.5-flash': False}
    POST isReasoningModel: {'gemini-1.5-pro': False, 'gemini-1.5-flash': False}
    controls (unchanged): gemini-2.5-pro=True, gemini-pro=True, some-future-model=True
    8/8 request probes identical (all HTTP 200, identical bodies)
    capability warnings identical, still naming both gemini-1.5 ids
    identical overall: True
    ```
    
    So aimock still mocks both ids identically, `isReasoningModel()` still
    returns
    `false` for both, and the advertised model list is unchanged.
    
    **Release mechanism.** `publish-release.yml` triggers on push to `main`,
    compares
    `package.json`'s version against npm, and publishes + tags only when
    that version is
    not yet published — i.e. releases come from a **manual `package.json`
    version bump**.
    There is no Changesets setup (no `.changeset/`, no `@changesets/cli`),
    no
    release-please and no semantic-release, and no workflow derives a
    version from commit
    messages, so **the conventional-commit type does not trigger a
    release**; the
    `fix(drift):` prefix here matches the repo's own prior art for registry
    classification changes (#315, #322) and is inert with respect to
    versioning.
    `publish-commit.yml` → `pkg-pr-new` is just the per-PR preview publish
    behind the
    "Continuous Releases" / "preview" checks and gates nothing on
    versioning. **No
    changeset file and no version bump are required.**
    
    Minor pre-existing caveat: `unreleased-check.yml` counts commits
    touching `src/`
    since the last tag without excluding `src/__tests__/`, so on merge it
    will emit a
    `::warning::` that the version wasn't bumped. It is a warning only — no
    `exit 1` —
    so it does not fail CI. #336 had the same shape.
    
    ---
    
    ## Separate latent defect, filed for follow-up (NOT introduced by this
    PR)
    
    The *mechanical* registry-edit path documented for new-family notes is
    currently
    dead-locked by its own gate, so `ok-applied` can never happen: gate-2 of
    `drift-sync-check.ts` re-runs `logic-pin.test.ts` as a subprocess, that
    file's
    `DATA_FROZEN` block freezes set **membership**, and a mechanical
    add/remove *is* a
    membership change — so it trips the pin, gets reverted by `revertFiles`,
    and
    reports `gate-failed`. #335 shipped the mechanical path; #336 added
    `DATA_FROZEN`
    40 minutes later. It has never been observed because no `ok-applied` run
    has ever
    occurred. The `ok-applied` test injects a fake gate (`makeFakeDeps`), so
    it asserts
    `runSyncCheck` was *called*, never that the real pin would pass. Worth
    fixing
    separately — either exempt the `DATA_FROZEN` membership hashes from
    gate-2, or have
    drift-sync re-pin the hash as part of the allowlisted data edit and lean
    on human
    PR review as the guard.
    jpr5 authored Jul 24, 2026
    Configuration menu
    Copy the full SHA
    d1b5304 View commit details
    Browse the repository at this point in the history
  6. chore(drift): classify claude-opus-5 in includeFamilies (live GA text…

    … chat)
    
    The live /models family canary is red on main: Anthropic shipped
    `claude-opus-5` (Claude Opus 5, created_at 2026-07-24) and the registry
    has no classification for it, so `unclassifiedFamilies` flags it as a
    new-family decision and keeps routing it to a human every run.
    
    Classify it as INCLUDE — it is a GA text-chat/reasoning family on the
    live listing, exactly like its already-included siblings
    `claude-sonnet-5` and `claude-opus-4-8`. All 11 ids the live Anthropic
    listing returns are text-chat Claude families and every other one is
    already in includeFamilies.anthropic; opus-5 was simply missed because it
    landed after the last classification wave (34056c2).
    
    No reasoning-map change. `isReasoningModel("claude-opus-5")` already
    returns true via the documented fail-open default (no NONREASONING_FAMILIES
    prefix matches), which is the correct verdict for an extended-thinking
    Opus-class model. That matches the precedent set for claude-sonnet-5 and
    claude-fable-5, which also rely on the default rather than a
    REASONING_FAMILIES entry, so src/model-utils.ts is untouched.
    
    No mock/fixture work needed: aimock's Anthropic surface is fixture-driven
    and model-agnostic, so claude-opus-5 already behaves identically to the
    included claude-sonnet-5.
    
    Re-pin includeFamilies.anthropic membership — the DATA_FROZEN canary
    correctly trips on the deliberate addition. New pin derived independently
    from the test's own sha256(JSON.stringify(sorted)) and cross-checked
    against vitest's reported value.
    
    Registry data is test/script-only (no shipped importer, absent from the
    npm tarball, dist/ semantically identical pre/post), so this carries zero
    behavior delta and no version bump.
    jpr5 committed Jul 24, 2026
    Configuration menu
    Copy the full SHA
    72f85f8 View commit details
    Browse the repository at this point in the history
  7. chore(drift): classify claude-opus-5 in includeFamilies (live GA text…

    … chat) (#339)
    
    ## What was unclassified
    
    `claude-opus-5` — **Claude Opus 5** — is an unclassified Anthropic model
    family on `main`. It appears in Anthropic's live `GET /v1/models`
    listing but is in neither `includeFamilies.anthropic` nor
    `excludeFamilies.anthropic`, so the live `/models` family canary flags
    it as a new-family decision and re-routes it to a human on every run.
    
    It was simply missed: the last classification wave (`34056c2`) covered
    the 2026-07-16 listing, and Opus 5 landed after it (`created_at:
    2026-07-24`).
    
    **Only this one family is unclassified.** The live listing returns 11
    ids and every other one is already classified — including the other two
    Claude-5 members, `claude-sonnet-5` and `claude-fable-5`. So this is a
    one-family fix, not a wave.
    
    ## Evidence it was unclassified
    
    Live listing (`GET https://api.anthropic.com/v1/models`, fetched
    2026-07-24, key sourced from the session secret cache — never persisted
    anywhere):
    
    ```
    count 11  has_more False
      claude-opus-5              | Claude Opus 5    | 2026-07-24T00:00:00Z   <-- unclassified
      claude-sonnet-5            | Claude Sonnet 5  | 2026-06-29T00:00:00Z
      claude-fable-5             | Claude Fable 5   | 2026-06-07T00:00:00Z
      claude-opus-4-8            | Claude Opus 4.8  | 2026-05-28T00:00:00Z
      claude-opus-4-7            | Claude Opus 4.7  | 2026-04-14T00:00:00Z
      claude-sonnet-4-6          | Claude Sonnet 4.6| 2026-02-17T00:00:00Z
      claude-opus-4-6            | Claude Opus 4.6  | 2026-02-04T00:00:00Z
      claude-opus-4-5-20251101   | Claude Opus 4.5  | 2025-11-24T00:00:00Z
      claude-haiku-4-5-20251001  | Claude Haiku 4.5 | 2025-10-15T00:00:00Z
      claude-sonnet-4-5-20250929 | Claude Sonnet 4.5| 2025-09-29T00:00:00Z
      claude-opus-4-1-20250805   | Claude Opus 4.1  | 2025-08-05T00:00:00Z
    ```
    
    Independently corroborated by PR #337's `drift-live-pr` job, whose
    uploaded head **and** base drift reports both contain exactly one entry
    — this same finding — with `conclusion: critical`. It passed #337's
    delta gate only because that gate is new-in-head-only, so the finding
    has been sitting on `main` unaddressed.
    
    ## Classification chosen, and why
    
    **`includeFamilies.anthropic`**
    (`src/__tests__/drift/model-registry.ts:132`), under the existing `//
    Claude 5 families (text chat)` comment.
    
    Justification from the code, not from vibes:
    
    - `includeFamilies` is documented as "families aimock actually MOCKS …
    chat / text completion families" (`model-registry.ts:16-20`). Opus 5 is
    a GA text-chat/reasoning model on the live listing — squarely that
    category.
    - **Every** GA text-chat Claude family on the live listing is in
    `includeFamilies.anthropic`. `excludeFamilies.anthropic` holds only
    retired/legacy ids (`claude-v3`, `claude-2`, `claude-instant-1`,
    `model-registry.ts:217-222`). Opus 5 is brand new, not retired, so
    exclude would be wrong.
    - Direct sibling precedent: `claude-sonnet-5` and `claude-opus-4-8` are
    both `includeFamilies` entries added by `34056c2` ("INCLUDE (text chat /
    reasoning aimock mocks): … Anthropic all 4.x/5 point releases"). Opus 5
    is the same class of thing.
    - It is not a preview and not Gemma, so neither exclude-by-rule
    predicate (`PREVIEW_FAMILY`, `GEMMA_FAMILY`) applies.
    
    ## Reasoning vs non-reasoning: reasoning-capable, and no code change
    needed
    
    `isReasoningModel("claude-opus-5")` **already returns `true`**, before
    and after this PR. Verified empirically (not reasoned) by driving the
    real `src/model-utils.ts` export on both tree states:
    
    ```
                                PRE-fix   POST-fix
      claude-opus-5           ->  true      true
      claude-opus-5-20260724  ->  true      true
      claude-sonnet-5         ->  true      true
      claude-fable-5          ->  true      true
      claude-opus-4-8         ->  true      true
      claude-opus-4           ->  true      true
      claude-3-5-sonnet       ->  false     false     (control)
      claude-3-opus           ->  false     false     (control)
    ```
    
    `true` is the **correct** verdict for an extended-thinking Opus-class
    model. It arrives via the documented fail-open default
    (`model-utils.ts:18-26`, `135-136`): no `NONREASONING_FAMILIES` prefix
    matches `claude-opus-5`, and `REASONING_FAMILIES`' `claude-opus-4` entry
    does not prefix-match `claude-opus-5` either, so it falls through to
    `true`.
    
    So **`src/model-utils.ts` is deliberately untouched.** Adding a
    `REASONING_FAMILIES` entry would be a no-op — that list is explicitly
    "mostly documentation; because unknown ids default to `true` it is not
    load-bearing for correctness" (`model-utils.ts:21-22`) — and it would
    break the precedent that `claude-sonnet-5` and `claude-fable-5` also
    rely on the default rather than an explicit entry. Touching shipped
    source for a zero-delta documentation edit would turn a registry-only
    change into a src change for no benefit.
    
    ## Mock / fixture support: none needed
    
    Registry classification alone is sufficient, and this PR does **not**
    claim support aimock lacks.
    
    aimock's Anthropic surface is fixture-driven and model-agnostic — it
    matches on request content, not on model id. Probed the built `dist/`
    with real HTTP requests against a running server:
    
    ```
    POST /v1/messages {"model":"claude-opus-5",   ...}  -> {"error":{"message":"No fixture matched", ...}}
    POST /v1/messages {"model":"claude-sonnet-5", ...}  -> {"error":{"message":"No fixture matched", ...}}
    ```
    
    Byte-identical behavior to the already-included `claude-sonnet-5`: with
    a matching fixture loaded both serve it, without one both report `No
    fixture matched`. No per-family code path exists, so there is nothing
    family-specific to add. `DEFAULT_MODELS` (`src/server.ts:227`) is a
    separate static 5-id list that this registry does not feed and that no
    Claude-5 family was ever in.
    
    This is exactly the situation the existing `claude-fable-5` comment
    already documents — a family included ahead of a recorded fixture,
    intentionally.
    
    ## RED → GREEN
    
    The failure surface is the real live drift leg:
    `src/__tests__/drift/models.drift.ts:606-618`,
    `describe.skipIf(!process.env.ANTHROPIC_API_KEY)`, which calls
    `listAnthropicModels()` against the real API and asserts through
    `assertNoUnclassifiedFamilies`. **It gates on `ANTHROPIC_API_KEY` and is
    SKIPPED without one**, so reproducing the RED below requires
    credentials; a bare checkout silently skips it. A credential-free leg is
    included after.
    
    ### RED — unmodified `main` (d1b5304), live API
    
    ```
    $ pnpm exec vitest run --config vitest.config.drift.ts \
        src/__tests__/drift/models.drift.ts -t "live /models contains no unclassified family"
    
     × Anthropic Claude model-family availability > live /models contains no unclassified family 499ms
       →
    API DRIFT DETECTED: Anthropic Claude (live /models family canary)
    
      1. [critical] Unclassified model family "claude-opus-5" in anthropic /models — add it to includeFamilies (aimock mocks it) or excludeFamilies (non-text / retired / preview) in model-registry.ts
         Path:    models/claude-opus-5
         SDK:     (family in includeFamilies ∪ excludeFamilies)
         Real:    claude-opus-5
         Mock:    <no mock leg — live /models family canary>
    : expected [ 'claude-opus-5' ] to deeply equal []
    
     ❯ assertNoUnclassifiedFamilies src/__tests__/drift/models.drift.ts:95:32
     ❯ src/__tests__/drift/models.drift.ts:611:7
    
     Test Files  1 failed (1)
          Tests  1 failed | 24 skipped (25)
    ```
    
    ### GREEN — same command, after the classification
    
    ```
    $ pnpm exec vitest run --config vitest.config.drift.ts \
        src/__tests__/drift/models.drift.ts -t "live /models contains no unclassified family"
    
     ✓ src/__tests__/drift/models.drift.ts (25 tests | 24 skipped) 506ms
       ✓ Anthropic Claude model-family availability > live /models contains no unclassified family  505ms
    
     Test Files  1 passed (1)
          Tests  1 passed | 24 skipped (25)
    ```
    
    ### Credential-free corroboration
    
    Driving the pure mirror `unclassifiedFamiliesForSync`
    (`scripts/drift-sync.ts`) over the captured live payload, on both tree
    states:
    
    ```
    captured LIVE anthropic /models ids (11): ["claude-opus-5","claude-sonnet-5","claude-fable-5",
      "claude-opus-4-8","claude-opus-4-7","claude-sonnet-4-6","claude-opus-4-6",
      "claude-opus-4-5-20251101","claude-haiku-4-5-20251001","claude-sonnet-4-5-20250929",
      "claude-opus-4-1-20250805"]
    
    PRE-fix : unclassifiedFamiliesForSync(anthropic) = ["claude-opus-5"]   claude-opus-5 flagged = true
              includeFamilies.anthropic.has('claude-opus-5') = false
    POST-fix: unclassifiedFamiliesForSync(anthropic) = []                  claude-opus-5 flagged = false
              includeFamilies.anthropic.has('claude-opus-5') = true
              excludeFamilies.anthropic.has('claude-opus-5') = false
    ```
    
    ### Membership pin RED → GREEN
    
    The `DATA_FROZEN` canary correctly tripped on the deliberate addition:
    
    ```
     × classification-data membership freeze > freezes includeFamilies.anthropic membership 5ms
       → Frozen data set "includeFamilies.anthropic" membership changed (now: ["claude-3-5-haiku",
         "claude-3-5-sonnet","claude-3-7-sonnet","claude-3-haiku","claude-3-opus","claude-3-sonnet",
         "claude-fable-5","claude-haiku-4","claude-haiku-4-5","claude-opus-4","claude-opus-4-1",
         "claude-opus-4-5","claude-opus-4-6","claude-opus-4-7","claude-opus-4-8","claude-opus-5",
         "claude-sonnet-4","claude-sonnet-4-5","claude-sonnet-4-6","claude-sonnet-5"]). If this is a
         deliberate, reviewed addition/removal of a classified family, update its pin here; if not,
         it is a silent canary-silencing edit and must be reverted.
         Expected: "dbd8b4ef9afd50057143480d89db373886e7c19abe36ef9b4421456305ca2509"
         Received: "ab79ff332fadeff93c2678ebe3e0af7a6280ce6f0deb4694228e316944dfeb74"
    
     Test Files  1 failed (1)
          Tests  1 failed | 19 passed (20)
    ```
    
    The new pin was **derived independently**, not copied from vitest's
    `Received`. Reimplemented the test's own hashing
    (`sha256(JSON.stringify(members.sort()))`, `logic-pin.test.ts:59-61` +
    `:227`) in a standalone script, and first validated the method by
    reproducing the **pre-existing** pin exactly:
    
    ```
    PRE  derived : dbd8b4ef9afd50057143480d89db373886e7c19abe36ef9b4421456305ca2509
    PRE  in-repo : dbd8b4ef9afd50057143480d89db373886e7c19abe36ef9b4421456305ca2509
    PRE  METHOD VALIDATED: True
    POST derived : ab79ff332fadeff93c2678ebe3e0af7a6280ce6f0deb4694228e316944dfeb74
    ```
    
    Three-way agreement (derived / vitest `Received` / committed value).
    After re-pinning:
    
    ```
     ✓ src/__tests__/drift/logic-pin.test.ts (20 tests) 3ms
     Test Files  1 passed (1)
          Tests  20 passed (20)
    ```
    
    ## Full suite — zero regressions
    
    ```
    $ pnpm test
     Test Files  166 passed (166)
          Tests  4768 passed | 3 skipped (4771)
    ```
    
    ## Versioning verdict: no bump, no changeset
    
    **No behavior change**, so nothing to release. Three independent
    confirmations:
    
    1. **No shipped importer.** Every importer of `model-registry.ts` is a
    test or a repo script (`scripts/drift-sync.ts`,
    `scripts/drift-sync-check.ts`, and six `src/__tests__/` files).
    Non-test, non-script importers: **none**. `src/model-utils.ts`,
    `src/server.ts` and the `tsdown` entrypoints never touch it —
    `includeFamilies`/`excludeFamilies` are drift metadata, not a runtime
    allowlist.
    2. **Absent from the published tarball, and not inlined.**
    `package.json` `files` is an allowlist
    (`["dist","fixtures",".claude-plugin","skills","CHANGELOG.md"]`); `npm
    pack --dry-run` yields 576 files with **0** matching `__tests__` /
    `logic-pin` / `model-registry` / `src/`. `dist/` contains zero
    occurrences of `includeFamilies`, `excludeFamilies`, or `claude-opus-5`.
    3. **Built artifact semantically identical.** Compared pre-fix and
    post-fix `dist/`, normalizing the two known non-reproducible-build
    artifacts this repo has (tsdown's import-alias numbering, e.g.
    `node_http0` vs `node_http1`, and declaration sourcemaps), with two
    identical-source builds as the noise floor:
    
    ```
    noise floor (identical source):
      CONTROL: POST vs CTRL      files=285 differing=0 setdiff=0
    subject:
      SUBJECT: PRE-fix vs POST-fix   files=285 differing=0 setdiff=0
    VERDICT: PUBLISHED ARTIFACT SEMANTICALLY IDENTICAL
    ```
    
    Release mechanism is a manual `package.json` version bump consumed by
    `publish-release.yml`; there is no Changesets / release-please /
    semantic-release, and no workflow derives a version from commit
    messages. `chore(drift):` is therefore the honest prefix — this is a
    classification-only change with zero behavior delta, so neither `feat`
    nor `fix` is warranted.
    
    ## Pre-push gates
    
    ```
    $ pnpm format:check      → All matched files use Prettier code style!
    $ pnpm lint              → clean
    $ pnpm exec tsc --noEmit → clean
    $ pnpm test              → 166 files passed, 4768 passed, 0 failed
    $ pnpm build             → ✔ Build complete
    $ pnpm test:exports      → node10 🟢  node16 CJS 🟢  node16 ESM 🟢  bundler 🟢
    $ pnpm exec commitlint --from HEAD~1 --to HEAD → clean
    ```
    
    ## Pre-existing caveat (not introduced here)
    
    `.github/workflows/unreleased-check.yml` counts commits touching `src/`
    since the last tag without excluding `src/__tests__/`, so on merge it
    will emit `::warning:: … version has not been bumped`. It is a warning
    with no `exit 1` and does not fail CI. #336 and #337 have the same
    shape.
    
    ## Live-CI RED → GREEN (added after CI ran)
    
    `drift-live-pr` runs the drift suite against the **real** provider
    `/models` listings and uploads a base and a head report. On this PR
    ([run
    30132383648](https://github.com/CopilotKit/aimock/actions/runs/30132383648))
    those two artifacts are themselves the red-green, from CI rather than
    from my machine:
    
    ```
    BASE (main @ d1b5304)                    HEAD (this PR)
      "conclusion": "critical"                 "conclusion": "clean"
      entries: 1                               "entries": []
        - critical | claude-opus-5
    ```
    
    `main` is measurably red on this finding right now; this branch is
    clean. Full CI: **26 pass, 2 skipping, 0 fail** (28 rows).
    jpr5 authored Jul 24, 2026
    Configuration menu
    Copy the full SHA
    f988b1c View commit details
    Browse the repository at this point in the history
Loading