model-eol 0.6.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -8
- package/SPEC.md +5 -0
- package/bot/bot.mjs +60 -4
- package/bot/lib/common.mjs +10 -2
- package/bot/lib/eval.mjs +4 -1
- package/check.mjs +2 -1
- package/feeds/amazon.json +4 -4
- package/feeds/anthropic.json +224 -7
- package/feeds/cohere.json +130 -0
- package/feeds/mistral.json +320 -0
- package/feeds/openai.json +563 -44
- package/lib/feeds.mjs +23 -0
- package/lib/reports.mjs +10 -1
- package/package.json +1 -1
- package/schema/model-eol.bot-config.schema.json +1 -1
package/README.md
CHANGED
|
@@ -36,7 +36,7 @@ format instead of each scraping the same pages.
|
|
|
36
36
|
replacement, `distributions` for per-channel lifecycles, and publisher `policy`
|
|
37
37
|
floors. It is small enough that a provider could serve it at
|
|
38
38
|
`/.well-known/model-eol.json` in an afternoon.
|
|
39
|
-
- **`feeds/`** - <!-- feeds-status -->Amazon (4 entries), Anthropic (
|
|
39
|
+
- **`feeds/`** - <!-- feeds-status -->Amazon (4 entries), Anthropic (32 entries), Cohere (12 entries), Google (93 entries), Mistral (40 entries) and OpenAI (195 entries), generated from the providers' live deprecation pages plus the AWS Bedrock, Google Vertex AI, and Azure Foundry lifecycle pages, feed data generated 2026-09-09<!-- /feeds-status -->. Every
|
|
40
40
|
dated entry carries a source URL. Anthropic's "not sooner than" dates for
|
|
41
41
|
active models are included as `tentative` planning floors.
|
|
42
42
|
- **`check.mjs`** - zero-dependency CLI: CI gate, PR diff gate, inventory, CycloneDX
|
|
@@ -115,7 +115,8 @@ files. The checker, PR gate, schedule, SBOM export, and badge run with no
|
|
|
115
115
|
credentials at all. The bot needs a GitHub token, and a fine-grained PAT if you
|
|
116
116
|
want its PRs to trigger checks. Provider API keys are optional in exactly two
|
|
117
117
|
places: your own eval command, and the models-endpoint coverage in the feed
|
|
118
|
-
refresh.
|
|
118
|
+
refresh. Refresh accepts `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, `GEMINI_API_KEY`
|
|
119
|
+
(or `GOOGLE_API_KEY`), `MISTRAL_API_KEY`, and `COHERE_API_KEY`.
|
|
119
120
|
|
|
120
121
|
Both command-line binaries ship in one zero-dependency npm package. The test
|
|
121
122
|
suite packs that package, installs it into an empty directory with the network
|
|
@@ -306,10 +307,25 @@ until January 8, 2027.
|
|
|
306
307
|
|
|
307
308
|
`distributions` in the spec carries these per-channel dates. `--via <distributor>`
|
|
308
309
|
judges your repo by the channel you call. The distributor refresh keeps the
|
|
309
|
-
Bedrock and
|
|
310
|
-
|
|
310
|
+
Bedrock, Vertex, and Azure Foundry dates current from their lifecycle pages.
|
|
311
|
+
Bedrock combines its [legacy table](https://docs.aws.amazon.com/bedrock/latest/userguide/model-lifecycle-legacy.html)
|
|
312
|
+
with [model cards](https://docs.aws.amazon.com/bedrock/latest/userguide/model-cards.html).
|
|
313
|
+
Card floors become tentative distribution dates without announcements.
|
|
314
|
+
Under `--via aws-bedrock`, these floors remain `scheduled` and display "not sooner than".
|
|
315
|
+
A distributor's later date gives you more time for the same migration. It is not a reason to
|
|
311
316
|
skip it.
|
|
312
317
|
|
|
318
|
+
The later date is also the last day you can still call the old model, when the
|
|
319
|
+
feed gives it as an exact date. A tentative or earliest floor is "at least until",
|
|
320
|
+
and the old model may answer past it. When a retired or retiring model still
|
|
321
|
+
answers on another channel, `check` appends
|
|
322
|
+
`[still answers via aws-bedrock until 2026-10-14]` to the line, and the bot adds
|
|
323
|
+
a `## Capture window` section to its PRs and issues. If your eval compares the
|
|
324
|
+
replacement's outputs against the old model's, capture that baseline before the
|
|
325
|
+
window closes. model-eol reports the window from the feeds and never runs the
|
|
326
|
+
capture; see [docs/DESIGN_EVAL_BOUNDARY.md](docs/DESIGN_EVAL_BOUNDARY.md) for
|
|
327
|
+
where that line sits and why.
|
|
328
|
+
|
|
313
329
|
## Policy floors
|
|
314
330
|
|
|
315
331
|
A feed entry with no `announced` and no `shutdown` means "nothing scheduled as
|
|
@@ -440,6 +456,8 @@ receives:
|
|
|
440
456
|
- `MODEL_EOL_OLD_ID` and `MODEL_EOL_NEW_ID` for the current migration.
|
|
441
457
|
- `MODEL_EOL_PLAN`, a one-model plan containing the exact changed references.
|
|
442
458
|
- `MODEL_EOL_REPORT`, the required path for a bounded, publishable Markdown receipt.
|
|
459
|
+
- `MODEL_EOL_VIA`, the distributor channel the plan was built for, or empty for the publisher's direct API. Use it to map the publisher's replacement ID to your client's ID on that channel.
|
|
460
|
+
- `MODEL_EOL_EVAL_MODE`, always `evaluate` today. The value `capture` is reserved for a future run on the unpatched checkout against the old ID. The bot does not run it, and the hook refuses any other value.
|
|
443
461
|
|
|
444
462
|
Exit zero plus a regular report file means pass. Any other exit, a timeout, a
|
|
445
463
|
missing report, a tracked workspace change, or checkout drift fails that
|
|
@@ -505,12 +523,28 @@ read-only evaluation job, and write tokens live only in the reconciliation job.
|
|
|
505
523
|
|
|
506
524
|
```sh
|
|
507
525
|
node refresh/refresh.mjs --check # semantic diff vs live pages; exit 3 = PR-worthy
|
|
508
|
-
node refresh/refresh.mjs --distributor aws-bedrock,vertex-ai # distributor lifecycle clocks
|
|
526
|
+
node refresh/refresh.mjs --distributor aws-bedrock,vertex-ai,azure-ai-foundry # distributor lifecycle clocks
|
|
509
527
|
node scripts/feed-changelog.mjs # local rendering of the hosted Atom feed
|
|
510
528
|
```
|
|
511
529
|
|
|
512
530
|
A parse failure stops the run. The refresh never writes a guessed feed.
|
|
513
531
|
|
|
532
|
+
Anthropic reads model IDs and aliases from overview pages listed in `https://platform.claude.com/docs/llms.txt`.
|
|
533
|
+
It reads at most 100 unique pages with 30-second deadlines and 8 MiB response limits.
|
|
534
|
+
Only the deprecations page supplies Anthropic dates.
|
|
535
|
+
Offline checks use `anthropic-llms.txt` and `anthropic-model-pages/<slug>.md`; missing pages stop refresh.
|
|
536
|
+
Alias attachments and moves emit notices and appear in semantic diffs.
|
|
537
|
+
|
|
538
|
+
AWS changed its Bedrock lifecycle policy on 2026-09-07.
|
|
539
|
+
The legacy table covers models launched before that date; newer models publish lifecycle dates on individual cards.
|
|
540
|
+
The Bedrock refresh reads both sources and keeps legacy-table records when IDs overlap.
|
|
541
|
+
Conflicting exact dates appear under "Source conflicts"; the legacy table wins.
|
|
542
|
+
When two card floors differ, the later day-precision date applies.
|
|
543
|
+
Cards without lifecycle fields or day-precision dates appear under "Model cards without lifecycle fields" and emit notices.
|
|
544
|
+
The crawl reads at most 400 unique cards sequentially, with 30-second deadlines and 8 MiB response limits.
|
|
545
|
+
Offline checks use `bedrock-lifecycle.html`, `bedrock-model-cards.html`, and `bedrock-model-cards/<basename>` fixtures.
|
|
546
|
+
Missing card fixtures stop the refresh.
|
|
547
|
+
|
|
514
548
|
This runs automatically. A weekly workflow
|
|
515
549
|
(`.github/workflows/feed-refresh.yml`, Mondays 05:23 UTC) checks the live
|
|
516
550
|
sources and opens a PR with the semantic diff, plus an updated README freshness
|
|
@@ -566,9 +600,9 @@ is the most useful thing any of us can do here.
|
|
|
566
600
|
- The checker matches known IDs only. It does not discover models that are
|
|
567
601
|
absent from the feeds. This is deliberate: a CI gate needs precision more than
|
|
568
602
|
discovery.
|
|
569
|
-
- Fetchers: OpenAI, Anthropic, Google, aws-bedrock,
|
|
570
|
-
|
|
571
|
-
|
|
603
|
+
- Fetchers: OpenAI, Anthropic, Google, Mistral, Cohere, aws-bedrock, vertex-ai,
|
|
604
|
+
and azure-ai-foundry are live. xAI publishes no lifecycle index and its models
|
|
605
|
+
API carries no deprecation fields, so xAI is not ingested.
|
|
572
606
|
- On npm as [`model-eol`](https://www.npmjs.com/package/model-eol). Material
|
|
573
607
|
feed changes republish automatically as patch versions, with trusted
|
|
574
608
|
publishing and provenance. Code releases require an explicit stable version.
|
package/SPEC.md
CHANGED
|
@@ -69,6 +69,11 @@ Private arrangements after EOL are not represented by this status. `retired`
|
|
|
69
69
|
means the distributor explicitly reports the model as no longer generally
|
|
70
70
|
available even when it does not publish an exact shutdown date.
|
|
71
71
|
|
|
72
|
+
Bedrock model-card floors are tentative distribution dates (`date_precision: "tentative"`) without `announced`.
|
|
73
|
+
|
|
74
|
+
Azure maps GA/Preview/Legacy to `active`, Deprecated to `legacy`, and Retired to `retired`; Azure Legacy permits new deployments.
|
|
75
|
+
Azure OpenAI uses a resolving bare identity for single-row models only when a dated snapshot candidate is absent.
|
|
76
|
+
|
|
72
77
|
A model may contain at most one distribution for each `via`. Duplicate channel
|
|
73
78
|
records are ambiguous and fail runtime semantic validation. Draft-07 can enforce
|
|
74
79
|
whole-object uniqueness, but cannot express uniqueness keyed by the `via` field,
|
package/bot/bot.mjs
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
stableJson,
|
|
26
26
|
} from './lib/common.mjs'
|
|
27
27
|
import { assertValidPlanDocument } from '../lib/validate-document.mjs'
|
|
28
|
+
import { captureWindowFor, lifecycleFor } from '../lib/feeds.mjs'
|
|
28
29
|
import { parseCliArgs } from '../lib/cli.mjs'
|
|
29
30
|
import { loadConfig } from './lib/config.mjs'
|
|
30
31
|
import { downloadFeeds } from './lib/feeds.mjs'
|
|
@@ -363,11 +364,21 @@ const buildModelGroups = (plan, config, root, records) => {
|
|
|
363
364
|
}
|
|
364
365
|
for (const group of groups.values()) {
|
|
365
366
|
group.items.sort((a, b) => `${a.file}:${a.line}:${a.occurrence}`.localeCompare(`${b.file}:${b.line}:${b.occurrence}`))
|
|
366
|
-
group.feedDigest =
|
|
367
|
+
group.feedDigest = groupDigest(group.items, group.context?.entry)
|
|
367
368
|
}
|
|
368
369
|
return [...groups.values()].sort((a, b) => `${a.publisher}/${a.id}`.localeCompare(`${b.publisher}/${b.id}`))
|
|
369
370
|
}
|
|
370
371
|
|
|
372
|
+
// the capture section reads clocks outside the plan items, so they join the digest that gates body updates
|
|
373
|
+
const clockRows = entry => entry
|
|
374
|
+
? {
|
|
375
|
+
shutdown: entry.shutdown ?? null,
|
|
376
|
+
date_precision: entry.date_precision ?? null,
|
|
377
|
+
distributions: itemDigest((entry.distributions ?? []).map(row => ({ via: row.via, shutdown: row.shutdown ?? null, status: row.status ?? null, date_precision: row.date_precision ?? null }))),
|
|
378
|
+
}
|
|
379
|
+
: null
|
|
380
|
+
const groupDigest = (items, entry) => sha256(stableJson({ items: itemDigest(items), clocks: clockRows(entry) }))
|
|
381
|
+
|
|
371
382
|
const buildIssueGroups = (plan, config, root, records) => {
|
|
372
383
|
const ignoredIds = ignoredModelIds(plan, config)
|
|
373
384
|
const groups = new Map()
|
|
@@ -398,7 +409,7 @@ const buildIssueGroups = (plan, config, root, records) => {
|
|
|
398
409
|
}
|
|
399
410
|
for (const group of groups.values()) {
|
|
400
411
|
group.issues.sort((a, b) => `${a.file}:${a.line}:${a.reason}`.localeCompare(`${b.file}:${b.line}:${b.reason}`))
|
|
401
|
-
group.feedDigest =
|
|
412
|
+
group.feedDigest = groupDigest(group.issues, group.context?.entry)
|
|
402
413
|
}
|
|
403
414
|
return [...groups.values()].sort((a, b) => `${a.publisher}/${a.subject}/${a.shutdown ?? ''}`.localeCompare(`${b.publisher}/${b.subject}/${b.shutdown ?? ''}`))
|
|
404
415
|
}
|
|
@@ -484,6 +495,43 @@ const replacementSection = (item, now) => [
|
|
|
484
495
|
: []),
|
|
485
496
|
]
|
|
486
497
|
|
|
498
|
+
const captureWindow = (group, now) => {
|
|
499
|
+
const entry = group.context?.entry
|
|
500
|
+
if (!entry) return null
|
|
501
|
+
const lifecycle = lifecycleFor(entry, {
|
|
502
|
+
days: group.items?.[0]?.threshold_days ?? group.issues?.[0]?.threshold_days ?? 0,
|
|
503
|
+
via: group.via ?? null,
|
|
504
|
+
today: now,
|
|
505
|
+
})
|
|
506
|
+
return captureWindowFor(entry, lifecycle, { today: now })
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// the first date on which the capture section changes, so a body can be refreshed when the digest cannot see it
|
|
510
|
+
const captureExpiry = capture => capture
|
|
511
|
+
? [capture.until, ...capture.alternatives.map(alternative => alternative.until)].filter(Boolean).sort()[0] ?? null
|
|
512
|
+
: null
|
|
513
|
+
|
|
514
|
+
const captureExpired = (metadata, now) => typeof metadata.capture_expires === 'string' && metadata.capture_expires <= now.toISOString().slice(0, 10)
|
|
515
|
+
|
|
516
|
+
const captureSection = (group, now) => {
|
|
517
|
+
const capture = captureWindow(group, now)
|
|
518
|
+
if (!capture) return ''
|
|
519
|
+
const clock = capture.via === 'publisher' || capture.via === 'publisher-fallback'
|
|
520
|
+
? markdownText('publisher')
|
|
521
|
+
: markdownCode(capture.via)
|
|
522
|
+
const untilText = window => `${['tentative', 'earliest'].includes(window.date_precision) ? 'at least until' : 'until'} ${markdownText(window.until)}`
|
|
523
|
+
return [
|
|
524
|
+
'## Capture window',
|
|
525
|
+
capture.until !== null
|
|
526
|
+
? `- The old model answers on the ${clock} clock ${untilText(capture)}.`
|
|
527
|
+
: `- The old model no longer answers on the ${clock} clock. No baseline can be captured from it there.`,
|
|
528
|
+
...capture.alternatives.map(alternative => `- It still answers via ${markdownCode(alternative.via)} ${untilText(alternative)}.`),
|
|
529
|
+
capture.until === null && capture.alternatives.length === 0
|
|
530
|
+
? '- The feed lists no dated clock on which the old model still answers, so model-eol cannot state a capture window. model-eol does not run captures.'
|
|
531
|
+
: '- Capture a baseline from the old model before the window closes if your eval compares outputs. model-eol does not run captures.',
|
|
532
|
+
].join('\n')
|
|
533
|
+
}
|
|
534
|
+
|
|
487
535
|
export const buildPullBody = ({ group, headSha, baseSha = null, now = new Date(), tokenKind = null, evalResult = null, evalConfigHash = null }) => {
|
|
488
536
|
const item = group.items[0]
|
|
489
537
|
const announced = markdownText(group.context?.announced ?? 'not specified')
|
|
@@ -500,6 +548,7 @@ export const buildPullBody = ({ group, headSha, baseSha = null, now = new Date()
|
|
|
500
548
|
base_sha: baseSha,
|
|
501
549
|
head_sha: headSha,
|
|
502
550
|
feed_digest: group.feedDigest,
|
|
551
|
+
capture_expires: captureExpiry(captureWindow(group, now)),
|
|
503
552
|
...(evalConfigHash ? { eval_config_digest: evalConfigHash } : {}),
|
|
504
553
|
}
|
|
505
554
|
const sections = [
|
|
@@ -522,6 +571,8 @@ export const buildPullBody = ({ group, headSha, baseSha = null, now = new Date()
|
|
|
522
571
|
if (group.via) {
|
|
523
572
|
sections.push('', '## Distributor clock', `This migration uses the ${markdownCode(group.via)} distributor clock; the shutdown date above is the date for that channel.`)
|
|
524
573
|
}
|
|
574
|
+
const capture = captureSection(group, now)
|
|
575
|
+
if (capture) sections.push('', capture)
|
|
525
576
|
const evaluation = evalSection(evalResult)
|
|
526
577
|
if (evaluation) sections.push('', evaluation)
|
|
527
578
|
sections.push(
|
|
@@ -547,6 +598,7 @@ export const buildIssueBody = ({ group, now = new Date() }) => {
|
|
|
547
598
|
replacement_note: issue.replacement_note,
|
|
548
599
|
head_sha: null,
|
|
549
600
|
feed_digest: group.feedDigest,
|
|
601
|
+
capture_expires: captureExpiry(captureWindow(group, now)),
|
|
550
602
|
...(group.channel ? { channel: group.channel } : {}),
|
|
551
603
|
}
|
|
552
604
|
const evidence = group.issues
|
|
@@ -580,6 +632,8 @@ export const buildIssueBody = ({ group, now = new Date() }) => {
|
|
|
580
632
|
'## Feed notes',
|
|
581
633
|
notesSection(group),
|
|
582
634
|
]
|
|
635
|
+
const capture = captureSection(group, now)
|
|
636
|
+
if (capture) sections.push('', capture)
|
|
583
637
|
const evaluation = evalSection(group.evalResult)
|
|
584
638
|
if (evaluation) sections.push('', evaluation)
|
|
585
639
|
return sections.join('\n')
|
|
@@ -841,6 +895,8 @@ export const evaluatePlan = async ({
|
|
|
841
895
|
cwd: clone,
|
|
842
896
|
oldId: group.id,
|
|
843
897
|
newId: group.items[0].replacement,
|
|
898
|
+
via: group.via ?? null,
|
|
899
|
+
mode: 'evaluate',
|
|
844
900
|
planPath: selectedPlanPath,
|
|
845
901
|
reportPath,
|
|
846
902
|
})
|
|
@@ -1090,7 +1146,7 @@ const processModel = async ({ api, pulls, issueRecords, group, source, base, bas
|
|
|
1090
1146
|
if (externalEval && externalEval.status !== 'pass') {
|
|
1091
1147
|
return processEvalFailure({ api, issueRecords, group, evalResult: externalEval, root, now, issuesEnabled: config.issues.enabled, open })
|
|
1092
1148
|
}
|
|
1093
|
-
if (open.metadata.feed_digest === currentDigest && open.metadata.base_sha === baseHead && (!evalConfigHash || open.metadata.eval_config_digest === evalConfigHash)) {
|
|
1149
|
+
if (open.metadata.feed_digest === currentDigest && open.metadata.base_sha === baseHead && (!evalConfigHash || open.metadata.eval_config_digest === evalConfigHash) && !captureExpired(open.metadata, now)) {
|
|
1094
1150
|
return decision(group, 'skip-unchanged', { number: open.item.number })
|
|
1095
1151
|
}
|
|
1096
1152
|
let patch
|
|
@@ -1250,7 +1306,7 @@ const processIssue = async ({ api, issues, group, now }) => {
|
|
|
1250
1306
|
const open = matches.find(record => isOpen(record.item))
|
|
1251
1307
|
const body = buildIssueBody({ group, now })
|
|
1252
1308
|
if (open) {
|
|
1253
|
-
if (open.metadata.feed_digest === group.feedDigest) return decision(group, 'skip-unchanged', { number: open.item.number, body })
|
|
1309
|
+
if (open.metadata.feed_digest === group.feedDigest && !captureExpired(open.metadata, now)) return decision(group, 'skip-unchanged', { number: open.item.number, body })
|
|
1254
1310
|
await api.updateIssue(open.item.number, { title: issueTitle(group), body })
|
|
1255
1311
|
return decision(group, 'update', { number: open.item.number, body })
|
|
1256
1312
|
}
|
package/bot/lib/common.mjs
CHANGED
|
@@ -16,7 +16,10 @@ const stableValue = value => {
|
|
|
16
16
|
export const stableJson = value => JSON.stringify(stableValue(value))
|
|
17
17
|
|
|
18
18
|
export const sortedJsonDigest = values => {
|
|
19
|
-
const sorted = values.slice().sort((a, b) =>
|
|
19
|
+
const sorted = values.slice().sort((a, b) => {
|
|
20
|
+
const [left, right] = [stableJson(a), stableJson(b)]
|
|
21
|
+
return left < right ? -1 : left > right ? 1 : 0
|
|
22
|
+
})
|
|
20
23
|
return sha256(stableJson(sorted))
|
|
21
24
|
}
|
|
22
25
|
|
|
@@ -67,6 +70,7 @@ export const parseMetadata = body => {
|
|
|
67
70
|
if (metadata.replacement !== undefined && metadata.replacement !== null && typeof metadata.replacement !== 'string') return null
|
|
68
71
|
if (metadata.replacement_options !== undefined && (!Array.isArray(metadata.replacement_options) || metadata.replacement_options.some(option => typeof option !== 'string'))) return null
|
|
69
72
|
if (metadata.replacement_note !== undefined && metadata.replacement_note !== null && typeof metadata.replacement_note !== 'string') return null
|
|
73
|
+
if (metadata.capture_expires !== undefined && metadata.capture_expires !== null && typeof metadata.capture_expires !== 'string') return null
|
|
70
74
|
if (metadata.base_sha !== undefined && metadata.base_sha !== null && (typeof metadata.base_sha !== 'string' || !/^[0-9a-f]{40,64}$/.test(metadata.base_sha))) return null
|
|
71
75
|
if (metadata.stale_closed !== undefined && metadata.stale_closed !== true) return null
|
|
72
76
|
if (typeof metadata.feed_digest !== 'string' || !metadata.feed_digest) return null
|
|
@@ -111,7 +115,11 @@ export const markdownText = value => String(value ?? '')
|
|
|
111
115
|
.map(line => line.replace(/^(\s*)([-+>]\s|\d+\.\s)/, '$1- '))
|
|
112
116
|
.join('\n')
|
|
113
117
|
|
|
114
|
-
|
|
118
|
+
// code spans show entities literally, so drop backticks and line breaks instead of escaping
|
|
119
|
+
export const markdownCode = value => {
|
|
120
|
+
const text = String(value ?? '').replace(/[\r\n]+/g, ' ').replaceAll('`', '')
|
|
121
|
+
return `\`${text === '' ? ' ' : text}\``
|
|
122
|
+
}
|
|
115
123
|
|
|
116
124
|
export const markdownLink = (label, value) => {
|
|
117
125
|
const text = String(value ?? '')
|
package/bot/lib/eval.mjs
CHANGED
|
@@ -50,7 +50,8 @@ export const readReportCapped = (file, limit) => {
|
|
|
50
50
|
}
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
-
export const runEvalHook = ({ command, timeoutMs, maxReportBytes, passEnv = [], cwd, oldId, newId, planPath, reportPath }) => {
|
|
53
|
+
export const runEvalHook = ({ command, timeoutMs, maxReportBytes, passEnv = [], cwd, oldId, newId, planPath, reportPath, via = null, mode = 'evaluate' }) => {
|
|
54
|
+
if (mode !== 'evaluate') throw new Error(`unsupported eval mode: ${mode}`)
|
|
54
55
|
if (fs.existsSync(reportPath)) fs.rmSync(reportPath, { force: true })
|
|
55
56
|
const env = {}
|
|
56
57
|
for (const key of ['PATH', 'HOME', 'TMPDIR', 'LANG', 'USER', 'SHELL']) {
|
|
@@ -63,6 +64,8 @@ export const runEvalHook = ({ command, timeoutMs, maxReportBytes, passEnv = [],
|
|
|
63
64
|
env.MODEL_EOL_NEW_ID = newId
|
|
64
65
|
env.MODEL_EOL_PLAN = planPath
|
|
65
66
|
env.MODEL_EOL_REPORT = reportPath
|
|
67
|
+
env.MODEL_EOL_VIA = via ?? ''
|
|
68
|
+
env.MODEL_EOL_EVAL_MODE = mode
|
|
66
69
|
|
|
67
70
|
const result = spawnSync(command, {
|
|
68
71
|
cwd,
|
package/check.mjs
CHANGED
|
@@ -406,7 +406,8 @@ if (command === 'plan') {
|
|
|
406
406
|
|
|
407
407
|
if (command === 'check') {
|
|
408
408
|
if (AS_JSON) {
|
|
409
|
-
|
|
409
|
+
// capture stays out of the 0.1 document until the contract grows; the human line and the bot carry it
|
|
410
|
+
console.log(JSON.stringify({ schema: 'model-eol/check@0.1', threshold_days: DAYS, distributor: VIA, scope: SCOPE, scan_notes: scan.notes, findings: changedFindings.map(({ capture, ...finding }) => finding) }, null, 2))
|
|
410
411
|
} else {
|
|
411
412
|
console.log(formatCheck({ findings: changedFindings, bad, scannedFiles: scan.files.length, days: DAYS, scope: SCOPE }))
|
|
412
413
|
}
|
package/feeds/amazon.json
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"announced": "2026-03-30",
|
|
16
16
|
"shutdown": "2026-09-30",
|
|
17
17
|
"status": "legacy",
|
|
18
|
-
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-lifecycle.html"
|
|
18
|
+
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-lifecycle-legacy.html"
|
|
19
19
|
}
|
|
20
20
|
]
|
|
21
21
|
},
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"announced": "2026-03-30",
|
|
31
31
|
"shutdown": "2026-09-30",
|
|
32
32
|
"status": "legacy",
|
|
33
|
-
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-lifecycle.html"
|
|
33
|
+
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-lifecycle-legacy.html"
|
|
34
34
|
}
|
|
35
35
|
]
|
|
36
36
|
},
|
|
@@ -45,7 +45,7 @@
|
|
|
45
45
|
"announced": "2026-03-13",
|
|
46
46
|
"shutdown": "2026-09-14",
|
|
47
47
|
"status": "legacy",
|
|
48
|
-
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-lifecycle.html"
|
|
48
|
+
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-lifecycle-legacy.html"
|
|
49
49
|
}
|
|
50
50
|
]
|
|
51
51
|
},
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"announced": "2026-03-13",
|
|
61
61
|
"shutdown": "2026-09-14",
|
|
62
62
|
"status": "legacy",
|
|
63
|
-
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-lifecycle.html"
|
|
63
|
+
"source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-lifecycle-legacy.html"
|
|
64
64
|
}
|
|
65
65
|
]
|
|
66
66
|
}
|