Skip to content

chore(models): refresh stale model defaults (NodeAgent) - #15

Open
HomenShum wants to merge 12 commits into
mainfrom
claude/model-refresh-nodeagent
Open

HomenShum wants to merge 12 commits into
mainfrom
claude/model-refresh-nodeagent

Conversation

@HomenShum

@HomenShum HomenShum commented Oct 14, 2026 •

Copy link
Copy Markdown
Owner

Summary

Refreshes stale model defaults in the live provider smoke and fixes the request shapes the new ids require.

Runtime sites (9: migrate 4, freeze 5, defer 0)

Line numbers in the two tables below are SPEC/inventory coordinates on origin/main (SPEC-v3 §A.2.3), so a reviewer can match them to the plan. Where the code now sits at head f7963e2: frozen chain scripts/nodeagent-live-provider-smoke.ts:68-71 (nex-agi/nex-n2-pro deleted), defaults :212 (openai) / :214 (anthropic) / :216 (gemini), env-chain resolver selectModelCandidates :202-210, openAiCompatibleChat :400.

Site Today Disposition Target / reason
scripts/nodeagent-live-provider-smoke.ts:34 z-ai/glm-5.2 freeze active on OpenRouter, no vendor-named successor; chain order is a cost decision -> Part B v2
:35 moonshotai/kimi-k2.7-code freeze same
:36 cohere/north-mini-code:free freeze same
:37 nvidia/nemotron-3-ultra-550b-a55b freeze same
:38 nex-agi/nex-n2-pro migrate (delete) absent from OpenRouter catalog 2026-10-10; also removed from .env.example:29 and README.md:172-176
:177 gpt-5.5 migrate gpt-6.1-sol
:179 claude-3-5-haiku-latest migrate claude-haiku-5-5 (retired 2026-02-19)
:181 gemini-2.0-flash migrate gemini-3.8-flash (shut down 2026-06-01)
nodekit.yaml:57 none freeze conformance mapping, no model

E / R sites (5: migrate 1, freeze 3, defer 1)

Site Class Disposition Reason
smoke.ts:173 env-chain resolver E freeze literals dispositioned above; order NODEAGENT_OPENROUTER_MODEL_PRIORITY ?? NODEAGENT_OPENROUTER_MODEL ?? const unchanged
smoke.ts:248 openAiCompatibleChat E migrate request shape fixed once here for OpenRouter and OpenAI
src/features/node-agent/providers/piAiAdapter.ts:34 E freeze provider + model are caller options
src/features/node-agent/protocol/model.ts:60 R freeze optional pass-through, no caller sets them
package.json:100 pi-ai pin R defer untouched; bump is a human change when a live line is wired

Class N (doc 4, example 2, other 2, test-assertion 1): docs loop = README.md default-priority sentence and .env.example chain only; no other doc/evidence file names a moved id (evidence trees are hash-bound, untouched).

Breaking-parameter fixes

  • temperature: 0 removed from OpenAI-compatible, Anthropic and Gemini bodies (400 on Haiku 5.5, removed on GPT-6, deprecated on Gemini 3.6+).
  • 16-token caps -> 256 (SMOKE_MAX_TOKENS) at all three sites (thinking counts toward the cap).
  • OpenAI branch: max_completion_tokens + reasoning_effort: "low", no temperature, no tools. OpenRouter branch keeps max_tokens, no temperature.
  • Anthropic reply text now read by content block type === "text"; a stop_reason: "refusal" response now throws provider_refusal before content is read (migration guide Haiku 5.5 checklist, [BLOCKS]).

Test proof

tests/liveProviderSmokeRequestShape.test.ts (20 cases: 6 request-shape incl. an Anthropic stop_reason: "refusal" regression, 14 receipt; the request-shape cases also assert the SPEC §1.3 forbidden keys absent: Gemini thinkingConfig.thinkingBudget/thinking_budget/candidateCount/candidate_count, OpenAI logprobs/top_logprobs. Each assertion observed to fail against a mutant that adds the key to the builder, mutant reverted) drives the script's own callProvider / selectModelCandidates / probeLlm / gitIdentity / receiptIdentity with a fake fetch. The git-identity cases run against a throwaway temp git repo, so the suite passes on a dirty or untracked working tree (verified with an untracked file in the checkout).

  • Request-shape cases fail against the old literals/shape. Re-proved at head 5ef49d8 (figure measured then, not re-measured since: at later heads the not-a-repo test asserts /not a git repository/i, so the exact count may differ) by restoring origin/main's scripts/nodeagent-live-provider-smoke.ts under the test as it was at 5ef49d8: 17 of 18 fail (first run at the first commit: 13 failed: expected 'claude-3-5-haiku-latest' to be 'claude-haiku-5-5', expected {...} to not have property "temperature").

  • Round-5 fixes, each observed to fail without its fix: (a) gitDirty case: the probe was *.tmp, ignored by .gitignore:44, so git never saw it and the case passed only on an already-dirty tree. The case now runs against an isolated temp git repo (makeRepo(), probe file probe.txt, no dependence on the working tree), asserting a clean tree, then that docs/eval output is ignored, then that an untracked root file gives gitDirty: true from both the repo root and docs/. With the :(top) anchors replaced by . the case fails with expected [ true, false ] to deeply equal [ true, true ]. (b) A successful reply costing 0.11 against maxUsd 0.10 now sets stoppedBy: "maxUsd"; (c) the receipt command quotes whitespace args. Against the previous head's script (3044810), (b) and (c) fail (2 failed | 13 passed).

  • Round-6 fixes: (a) budget.stoppedBy now compares the known (non-null) spend to maxUsd even when another row reported no cost; a retry after an HTTP 503 followed by a 0.11 reply stops the run (spentUsd stays null, stoppedBy: "maxUsd"; the test fails when the old spentUsd !== null guard is restored). (b) gitIdentity(cwd) replaces the git calls inside receiptIdentity; the gitDirty case uses a temp repo, so npm test / npm run check no longer depend on the state of the working tree. (c) main() captures testedCommit, gitDirty, constantSha256 before the first paid dispatch, so a non-git directory fails before any spend and a checkout change mid-run cannot misattribute the run. (d) The receipt command is POSIX shell-quoted (;, empty and ' arguments replay). (e) A missing provider key is detected before a request is counted: requests: 0, no row.

  • Round-7 fixes: (a) gitIdentity passes --untracked-files=all; the test sets status.showUntrackedFiles=no in the fixture and still expects gitDirty: true (fails without the flag). (b) The gitIdentity test now asserts gitDirty === false from the repo root and from docs/ after docs/eval/receipt.json is written and before the root probe (fails with a cwd-relative :(exclude)docs/eval), and asserts constantSha256 equals the sha256 of the committed blob. (c) The non-git test is retitled gitIdentity throws outside a git checkout; the call-before-dispatch ordering in main() is not covered by a test and is not claimed to be.

Check output (npm run prepush, the repo's declared check)

Branch rebased onto origin/main a918e85 (0 behind, 12 ahead). Re-run 2026-10-15 on a clean tree at head f7963e2 (git status clean before the run; no provider keys in the environment). npm run prepush exit 0: 12 test files, 137/137 tests, live smoke SKIP, build ok, 0 vulnerabilities. The request-shape file alone (npx vitest run tests/liveProviderSmokeRequestShape.test.ts): 20/20. The log is local to the author's machine and not part of the PR, so the output below is the evidence (earlier heads' counts, e.g. 135/135 at 32c56aa, no longer apply):

nodeagent live provider smoke: SKIP no provider key configured
...
 Test Files  12 passed (12)
      Tests  137 passed (137)
...
found 0 vulnerabilities
exit=0

prepush rewrites four docs/eval/*.json smoke outputs; those were restored (not committed). No paid provider calls were made.

Notes

  • .env.example:29 is uncommented, so anyone who copied it keeps the old list including nex-agi/nex-n2-pro; they should re-copy or remove that entry.

  • needs-live-eval (block, not a note): merge waits for keyed manual runs, one per migrated provider. The bare npm run nodeagent:live-provider:smoke is NOT sufficient: selectProvider picks the first keyed provider (openrouter, openai, anthropic, gemini), so on a machine with OPENROUTER_API_KEY it only tests the frozen chain, and the npm script hard-codes one --json-out. Required runs (all four before the label is removed; run each separately, attach each output):

    • npx tsx scripts/nodeagent-live-provider-smoke.ts --provider openai --model gpt-6.1-sol --json-out docs/eval/nodeagent-live-provider-smoke-openai.json (gpt-6.1-sol)
    • npx tsx scripts/nodeagent-live-provider-smoke.ts --provider anthropic --model claude-haiku-5-5 --json-out docs/eval/nodeagent-live-provider-smoke-anthropic.json (claude-haiku-5-5)
    • npx tsx scripts/nodeagent-live-provider-smoke.ts --provider gemini --model gemini-3.8-flash --json-out docs/eval/nodeagent-live-provider-smoke-gemini.json (gemini-3.8-flash)
    • npx tsx scripts/nodeagent-live-provider-smoke.ts --provider openrouter --json-out docs/eval/nodeagent-live-provider-smoke-openrouter.json (nex removal; frozen chain still reachable)

    --model is pinned on the three direct-provider runs because loadEnvFiles loads NODEAGENT_LIVE_MODEL and NODEAGENT_<PROVIDER>_MODEL from .env.local and selectModelCandidates honours them, so a stale override there would silently test another model (the receipt would be rejected under rule 3, but the paid run is wasted). The OpenRouter run is unpinned (no --model), and selectModelCandidates (scripts/nodeagent-live-provider-smoke.ts:205) checks NODEAGENT_LIVE_MODEL first, then NODEAGENT_OPENROUTER_MODEL_PRIORITY, then NODEAGENT_OPENROUTER_MODEL; any of the three replaces the frozen chain, and loadEnvFiles does not override variables already in the process env. Before the OpenRouter run, unset all three in the shell (unset NODEAGENT_LIVE_MODEL NODEAGENT_OPENROUTER_MODEL_PRIORITY NODEAGENT_OPENROUTER_MODEL) and remove them from .env.local and .env; after the run, check that the receipt's candidateIds equals the four-entry frozen chain (z-ai/glm-5.2, moonshotai/kimi-k2.7-code, cohere/north-mini-code:free, nvidia/nemotron-3-ultra-550b-a55b), otherwise the receipt is rejected under rule 3 and the run is wasted. Risk the keyed receipt may surface: Haiku 5.5 (adaptive thinking, medium effort by default) and gemini-3.8-flash (default thinking) count thinking tokens toward the spec's 256-token cap, so a reply could end in max_tokens with no text and the smoke would fail closed (expected NODEAGENT_LIVE_OK); the migration guide's lever then is effort low or a larger cap.

    Estimated cost: a few cents total (4 runs, one ~30-token reply each; the OpenRouter chain may make up to 4 attempts). Not run here (no paid calls).

  • Receipt schema (§A.0 rule 3) — fixed in this PR. The smoke report now carries command (POSIX shell-quoted, so semicolons, empty and spaced arguments replay), candidateIds, echoedModels, rows[{requestedModel, resolvedModel, echoedModel, usage, costUsd, fallbackUsed}], testedCommit (git rev-parse HEAD), gitDirty (top-anchored pathspecs and an explicit --untracked-files=all, so it is the same from any working directory and independent of the user's status.showUntrackedFiles; ignores docs/eval, the evidence directory), constantSha256 (this script as committed at testedCommit, read once from a single git rev-parse HEAD; recompute with git show <testedCommit>:scripts/nodeagent-live-provider-smoke.ts | sha256sum), budget{maxUsd, estimateUsd, spentUsd|null, requests, stoppedBy} and environment. echoedModel is the response model (Anthropic/OpenAI/OpenRouter) or modelVersion (Gemini); fallbackUsed is true when the OpenRouter chain moves past index 0; a budget stop (including a reply whose reported cost exceeds maxUsd, even when an earlier attempt reported no cost) sets ok: false. The git identity is read before the first dispatch. No model site, precedence or frozen site changed. Runtime behaviour does change in the smoke: the OpenRouter chain stops when committed spend (the reported cost, or the $0.01 estimate only where a row reports none) plus one more $0.01 estimate would exceed maxUsd $0.10; the cap is in dollars, not attempts, so cheap failed replies that report a small cost can run more than 10 attempts, and a budget stop fails the run (ok: false, exit 1). Fake-fetch tests assert a differing echoed model is recorded. Limits: costUsd is only filled where the provider reports it (OpenRouter usage.cost), otherwise null, and then budget.spentUsd is null (unknown), never 0. Before each dispatch the smoke requires committed spend (reported cost, or the $0.01 estimate for an unreported row) plus one more estimate to stay within maxUsd, so the cap holds for direct providers too. estimateUsd is a flat $0.01 per request, not a price table: it bounds the built-in and frozen ids (gpt-6.1-sol at 256 output tokens is about $0.0026), but an operator-supplied NODEAGENT_OPENROUTER_MODEL_PRIORITY of pricier ids can overshoot maxUsd by up to one request; such a run ends stoppedBy: "maxUsd", ok: false, so it is flagged, not silent. A refused Anthropic row records echoedModel and usage before the run fails. Merge still waits for the four keyed runs (not run here, no paid calls); a receipt with a fallback row, gitDirty, or stoppedBy is rejected and re-run.

  • SPEC GAP for Homen (§C), rule 10, not introduced by this PR: the smoke's explicit operator overrides pass an unknown id to the provider unchanged. --model <id> and NODEAGENT_LIVE_MODEL=<id> return [id] from selectModelCandidates (scripts/nodeagent-live-provider-smoke.ts:204-205 at head), and callProvider sends it as given (reproduced offline with a fake fetch: unverified-review-model was sent as model). §A.0 rule 10 says any other unknown id throws at the first resolver it reaches. This PR leaves it alone on purpose: the behaviour is identical on origin/main, §A.0 rule 4 says caller arguments and override precedence are untouched, and §A.2.3 freezes this resolver. Scope of the gap: no unknown id is a default (every built-in default is a §1.2 id or a frozen OpenRouter id), the operator must type the id themselves, and the default chain never turns a free-lane request into a paid one (a free-lane request is never rewritten). Not covered by that: an operator-supplied NODEAGENT_OPENROUTER_MODEL_PRIORITY list that mixes a :free id with a paid id can fall from the free id to the paid one when the free id fails; this is identical on origin/main and bounded by the maxUsd cap. Decision needed: either accept operator overrides as an explicit rule 4 carve-out of rule 10 for this smoke, or make the smoke reject ids outside the §1.2 set and the frozen chain in a follow-up (this changes how --model behaves).

  • Round-8 fixes (32c56aa): smoke budget wording no longer claims a hard bound; receipt command records npx tsx ... (replays verbatim); the git-spawning tests have a 30 s timeout (assertions unchanged); the not-a-repo test asserts /not a git repository/i.

  • Round-10 fix (8b12bea): gitIdentity read git rev-parse HEAD and then hashed git show HEAD:<script> in a separate call, so a checkout between the two would give a receipt naming commit A with commit B's hash. It now reads HEAD once and hashes <testedCommit>:<script>. A request-shape test moves HEAD between the two calls (via a pass-through node:child_process hook) and asserts the hash is the captured commit's blob; reverting the fix to HEAD: fails it. The PR-body changes in this round: --model on the keyed commands, the stale-override and thinking-token notes, and the mixed-list sentence. In the verifying prepush run, the first attempt had one 5000 ms timeout in the untouched tests/nodeAgentChatAdapter.test.ts (a load flake, 136/137); the immediate re-run was 137/137, exit 0.

  • Round-11 fixes (f7963e2, after rebase onto a918e85): the request-shape test now asserts the SPEC §1.3 forbidden keys absent (defect: a mutant adding thinkingConfig: { thinkingBudget: 0 }, candidateCount: 1 to the Gemini generationConfig passed 20/20; it now fails, as does a logprobs: true mutant on the OpenAI body). The keyed receipts must be taken at head f7963e2 or later so testedCommit is an ancestor of the PR head (rule 3); a receipt taken at the pre-rebase 8b12bea is not.

  • Round-9 fixes (b5f8e05): the end-of-request budget check now counts a row with no reported cost at the $0.01 estimate, the same accounting as the pre-dispatch reservation. Before, a 503 with no cost followed by a $0.095 reply (estimate total $0.105 vs maxUsd $0.10) ended ok: true, stoppedBy: null; now it ends stoppedBy: "maxUsd" while spentUsd stays null (a row is unknown). The new fake-fetch case fails against the previous script and passes now.

Spec: model-defaults SPEC Part A §A.2 (NodeAgent)

🤖 Generated with Claude Code

@HomenShum

Copy link
Copy Markdown
Owner Author

Live-eval attempt on f8d6f9a6b251a8c055dbe8a65db547bfaf53cffb (branch claude/model-refresh-nodeagent, clean tree): not run, no receipt produced.

Reason: no provider key was available on the machine that ran this. OPENAI_API_KEY, ANTHROPIC_API_KEY, GEMINI_API_KEY, GOOGLE_GENERATIVE_AI_API_KEY and OPENROUTER_API_KEY are unset in the process, user and machine environments, and the repo has no .env or .env.local (only .env.example). Spend: $0.00, 0 requests.

Result: passed = false. The changed roles (gpt-6.1-sol, claude-haiku-5-5, gemini-3.8-flash) are unverified live. The merge block stays: this PR still needs a receipt from a keyed machine. The needs-live-eval label does not exist in this repo, so the block is stated here and in the PR body only.

To produce the receipt (budget cap $1.5, one call per role), run once per role with the matching key set, e.g.
npx tsx scripts/nodeagent-live-provider-smoke.ts --provider openai --json-out docs/eval/nodeagent-live-provider-smoke.json
and likewise --provider anthropic and --provider gemini. Note the script writes one provider per report and does not record echoedModel/usage/cost, so the per-role reports need combining into the spec section A.0 rule 3 receipt shape.

@HomenShum
HomenShum force-pushed the claude/model-refresh-nodeagent branch from f8d6f9a to 5a8ab49 Compare October 14, 2026 18:37
HomenShum and others added 12 commits October 15, 2026 16:39
Live provider smoke defaults move off retired/shut-down ids and the
request shapes are fixed for the new targets (SPEC Part A §A.2 NodeAgent).

- gpt-5.5 -> gpt-6.1-sol, claude-3-5-haiku-latest -> claude-haiku-5-5,
  gemini-2.0-flash -> gemini-3.8-flash
- delete nex-agi/nex-n2-pro (absent from OpenRouter catalog 2026-10-10)
  from the priority list, .env.example and README
- drop temperature (400 on Haiku 5.5, removed on GPT-6, deprecated on
  Gemini 3.6+); 16-token caps -> 256; OpenAI branch sends
  max_completion_tokens + reasoning_effort low; Anthropic text read by type
- add request-shape test (fake fetch) for all four providers

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…read

- clear NODEAGENT_*_MODEL overrides in beforeEach so a host-exported
  override no longer fails the default-resolution assertions
- give the non-text fixture block a text field and assert it is excluded,
  so reverting the type filter in the Anthropic parser now fails the test

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ression test

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…ider smoke

The smoke report recorded only the requested model, so none of the four
keyed runs could produce a receipt review would accept. Add the rule 3
fields; no model site, precedence or request shape changes.

- builders keep the response `model` (Gemini `modelVersion`) and usage;
  OpenRouter `usage.cost` is recorded as costUsd, null elsewhere
- rows[{requestedModel, resolvedModel, echoedModel, usage, costUsd,
  fallbackUsed}], candidateIds, echoedModels, command, environment
- testedCommit (git rev-parse HEAD), gitDirty (ignores docs/eval),
  constantSha256 (this file at HEAD), budget with maxUsd stop
- fake-fetch tests: echoed model differs from requested and is recorded;
  fallbackUsed set when the OpenRouter chain moves past index 0

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…efusal row metadata

spentUsd is null when any dispatched row has no reported cost (direct OpenAI,
Anthropic, Gemini); the maxUsd guard reserves the per-request estimate before
dispatch. gitDirty uses top-anchored pathspecs so it no longer depends on the
caller's cwd. A refused Anthropic row now records echoedModel and usage.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…replies; quote receipt command

The gitDirty probe was named *.tmp, which .gitignore:44 ignores, so git status
never saw it and the test only passed on an already-dirty tree. The probe is
now gitdirty-probe.txt, asserted not ignored, with a clean-tree precondition.
A successful reply costing more than maxUsd now sets stoppedBy=maxUsd. The
receipt command quotes whitespace arguments so it can be replayed.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…command, request accounting

An unreported-cost row no longer hides an over-budget reply: the known spend is
compared to maxUsd even when spentUsd is null (retry after a 503, then a $0.11
reply, now sets stoppedBy). testedCommit, gitDirty and constantSha256 are
captured by gitIdentity() before the first dispatch, so a non-git directory
fails before any spend and a mid-run checkout change cannot misattribute the
run. The receipt command is POSIX shell-quoted (semicolons, empty and quote
characters replay). The credential is checked before a request is counted, so a
missing key records requests:0 and no row. The gitDirty test now runs against an
isolated temp git repo and no longer depends on the working tree.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…s catch the regressions they name

gitIdentity() now passes --untracked-files=all, so a user's
status.showUntrackedFiles=no can no longer make an untracked file report
gitDirty:false (SPEC-v3 A.0 rule 3 provenance). The gitIdentity test sets that
config in the fixture and still expects gitDirty:true. It also asserts the
docs/eval receipt leaves the tree clean from both the repo root and docs/
before any root probe file exists (catches a cwd-relative exclude), and
constantSha256 equals the sha256 of the committed blob rather than only
matching 64 hex characters. The non-git test title no longer claims main()
ordering it does not exercise.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… replayable npx command; git tests get a time budget and assert the git error

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
… at the estimate

The post-request check summed only reported costs, so a 503 with no cost followed by a
0.095 reply (estimate total 0.105 > maxUsd 0.10) ended ok:true, stoppedBy:null. It now
uses the same accounting as the pre-dispatch reservation. spentUsd stays null when any row
is unknown. Adds the 503-then-0.095 fake-fetch case.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…at that sha

constantSha256 used `git show HEAD:...` after a separate `git rev-parse HEAD`, so a checkout between the two calls produced a receipt naming commit A with commit B's hash. Hash `<testedCommit>:<script>` instead; a test moves HEAD between the two calls and asserts the hash belongs to the captured sha.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
…nking_budget, candidate_count, logprobs)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
@HomenShum
HomenShum force-pushed the claude/model-refresh-nodeagent branch from 8b12bea to f7963e2 Compare October 15, 2026 23:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

needs-live-eval merge blocked until keyed receipt attached

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant