model-eol 0.3.0 → 0.4.1

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.
@@ -67,6 +67,8 @@ export const parseMetadata = body => {
67
67
  if (metadata.replacement !== undefined && metadata.replacement !== null && typeof metadata.replacement !== 'string') return null
68
68
  if (metadata.replacement_options !== undefined && (!Array.isArray(metadata.replacement_options) || metadata.replacement_options.some(option => typeof option !== 'string'))) return null
69
69
  if (metadata.replacement_note !== undefined && metadata.replacement_note !== null && typeof metadata.replacement_note !== 'string') return null
70
+ 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
+ if (metadata.stale_closed !== undefined && metadata.stale_closed !== true) return null
70
72
  if (typeof metadata.feed_digest !== 'string' || !metadata.feed_digest) return null
71
73
  if (!Object.hasOwn(metadata, 'head_sha') || (metadata.head_sha !== null && typeof metadata.head_sha !== 'string')) return null
72
74
  if (metadata.channel !== undefined && (typeof metadata.channel !== 'string' || !metadata.channel)) return null
package/bot/lib/eval.mjs CHANGED
@@ -9,6 +9,12 @@ const truncateReport = (bytes, limit, truncated) => {
9
9
  return Buffer.concat([bytes.subarray(0, limit - marker.length), marker]).subarray(0, limit).toString('utf8')
10
10
  }
11
11
 
12
+ export const capReport = (report, limit) => {
13
+ if (report === null || report === undefined) return null
14
+ const bytes = Buffer.from(String(report), 'utf8')
15
+ return truncateReport(bytes, Math.max(0, limit), bytes.length > limit)
16
+ }
17
+
12
18
  export const readReportCapped = (file, limit) => {
13
19
  const cap = Math.max(0, limit)
14
20
  let lstat
package/bot/lib/git.mjs CHANGED
@@ -105,23 +105,26 @@ export const verifyBotBranch = (cwd, branch, expectedHead, auth = null) => {
105
105
  }
106
106
  }
107
107
 
108
- export const pushBranch = (cwd, branch, expectedHead = null, auth = null) => {
108
+ export const pushBranch = (cwd, branch, expectedHead = null, auth = null, { allowMissing = false } = {}) => {
109
109
  const destination = `HEAD:refs/heads/${branch}`
110
110
  if (expectedHead !== null) {
111
111
  const state = verifyBotBranch(cwd, branch, expectedHead, auth)
112
- if (!state.safe) {
112
+ if (!state.safe && !(allowMissing && !state.error && state.head === null)) {
113
113
  const error = new Error(state.error || `refusing force-push: branch head ${state.head || 'missing'} or committer email ${state.committerEmail || 'missing'} failed bot lease checks`)
114
114
  error.code = 'MODEL_EOL_BRANCH_STAND_DOWN'
115
115
  error.currentHead = state.head
116
116
  error.committerEmail = state.committerEmail
117
117
  throw error
118
118
  }
119
- run(cwd, [
120
- 'push',
121
- `--force-with-lease=refs/heads/${branch}:${expectedHead}`,
122
- 'origin',
123
- destination,
124
- ], false, auth)
119
+ if (state.head === null) run(cwd, ['push', 'origin', destination], false, auth)
120
+ else {
121
+ run(cwd, [
122
+ 'push',
123
+ `--force-with-lease=refs/heads/${branch}:${expectedHead}`,
124
+ 'origin',
125
+ destination,
126
+ ], false, auth)
127
+ }
125
128
  } else {
126
129
  run(cwd, ['push', 'origin', destination], false, auth)
127
130
  }
@@ -81,7 +81,7 @@ export class GitHubClient {
81
81
  } catch (error) {
82
82
  if (error.status !== 404) {
83
83
  this.modelEolLabelAvailable = false
84
- this.warn(`model-eol: warning: could not check the model-eol label; continuing without labels (${error.message})`)
84
+ this.warn(`model-eol: warning: could not check the model-eol label; refusing to publish untrusted work (${error.message})`)
85
85
  return false
86
86
  }
87
87
  }
@@ -97,11 +97,11 @@ export class GitHubClient {
97
97
  return true
98
98
  } catch {
99
99
  // Fall through to the warning below. Another writer may have raced us,
100
- // but a failed confirmation should never block the migration itself.
100
+ // but ownership must be confirmed before publishing work.
101
101
  }
102
102
  }
103
103
  this.modelEolLabelAvailable = false
104
- this.warn(`model-eol: warning: could not create the model-eol label; continuing without labels (${error.message})`)
104
+ this.warn(`model-eol: warning: could not create the model-eol label; refusing to publish untrusted work (${error.message})`)
105
105
  return false
106
106
  }
107
107
  }
@@ -113,7 +113,7 @@ export class GitHubClient {
113
113
  return true
114
114
  } catch (error) {
115
115
  this.modelEolLabelAvailable = false
116
- this.warn(`model-eol: warning: could not label #${number}; the migration was still published (${error.message})`)
116
+ this.warn(`model-eol: warning: could not label #${number}; refusing to leave it open as trusted bot work (${error.message})`)
117
117
  return false
118
118
  }
119
119
  }
@@ -157,7 +157,10 @@ export class GitHubClient {
157
157
  async createPull(payload) {
158
158
  const { data } = await this.request('POST', `/repos/${this.repo}/pulls`, payload)
159
159
  if (data?.number === undefined) throw new Error('GitHub pull request response did not contain a number')
160
- await this.addModelEolLabel(data.number)
160
+ if (!await this.addModelEolLabel(data.number)) {
161
+ try { await this.updatePull(data.number, { state: 'closed' }) } catch {}
162
+ throw new Error(`refusing unlabeled bot pull request #${data.number}; it was closed because ownership could not be established`)
163
+ }
161
164
  return data
162
165
  }
163
166
 
@@ -170,20 +173,8 @@ export class GitHubClient {
170
173
  }
171
174
 
172
175
  async createIssue(payload) {
173
- const body = this.modelEolLabelAvailable
174
- ? payload
175
- : { ...payload, labels: (payload.labels ?? []).filter(label => label !== MODEL_EOL_LABEL.name) }
176
- try {
177
- return (await this.request('POST', `/repos/${this.repo}/issues`, body)).data
178
- } catch (error) {
179
- if (error.status !== 422 || !(body.labels ?? []).includes(MODEL_EOL_LABEL.name)) throw error
180
- this.modelEolLabelAvailable = false
181
- this.warn(`model-eol: warning: issue labels were rejected; retrying without the model-eol label (${error.message})`)
182
- return (await this.request('POST', `/repos/${this.repo}/issues`, {
183
- ...body,
184
- labels: body.labels.filter(label => label !== MODEL_EOL_LABEL.name),
185
- })).data
186
- }
176
+ if (!this.modelEolLabelAvailable) throw new Error('refusing unlabeled bot issue; model-eol label is unavailable')
177
+ return (await this.request('POST', `/repos/${this.repo}/issues`, payload)).data
187
178
  }
188
179
 
189
180
  async updateIssue(number, payload) {
package/bot.yml.example CHANGED
@@ -15,98 +15,134 @@ env:
15
15
 
16
16
  jobs:
17
17
  plan:
18
- name: plan and evaluate
18
+ name: build trusted plan
19
19
  runs-on: ubuntu-latest
20
20
  permissions:
21
21
  contents: read
22
22
  steps:
23
- # The eval job must never hold write tokens.
24
- - uses: actions/checkout@v4
23
+ - uses: actions/checkout@v7
25
24
  with:
26
25
  persist-credentials: false
27
- - uses: actions/setup-node@v4
26
+ - uses: actions/setup-node@v7
28
27
  with:
29
- node-version: 20
28
+ node-version: 22
29
+ package-manager-cache: false
30
30
  - name: Build the versioned plan
31
31
  run: |
32
+ mkdir -p "$RUNNER_TEMP/model-eol-plan"
33
+ MODEL_EOL_VERSION="$(npm view "$MODEL_EOL_PACKAGE" version --json | node -e 'let s="";process.stdin.on("data",c=>s+=c).on("end",()=>{const v=JSON.parse(s);const r=Array.isArray(v)?v.at(-1):v;if(typeof r!=="string")process.exit(1);process.stdout.write(r)})')"
34
+ if [[ ! "$MODEL_EOL_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
35
+ echo "refusing invalid resolved model-eol version: $MODEL_EOL_VERSION" >&2
36
+ exit 1
37
+ fi
38
+ printf '%s\n' "$MODEL_EOL_VERSION" > "$RUNNER_TEMP/model-eol-plan/model-eol-version"
32
39
  PLAN_ARGS=(plan .)
33
40
  if [ ! -f .model-eol.json ]; then PLAN_ARGS+=(--days 90 --scope direct); fi
34
- npx --yes --package="$MODEL_EOL_PACKAGE" model-eol "${PLAN_ARGS[@]}" > model-eol-plan.json
35
- - name: Run the configured eval harness
41
+ npx --yes --package="model-eol@$MODEL_EOL_VERSION" model-eol "${PLAN_ARGS[@]}" > "$RUNNER_TEMP/model-eol-plan/model-eol-plan.json"
42
+ - name: Upload plan
43
+ uses: actions/upload-artifact@v7
44
+ with:
45
+ name: model-eol-plan
46
+ path: |
47
+ ${{ runner.temp }}/model-eol-plan/model-eol-plan.json
48
+ ${{ runner.temp }}/model-eol-plan/model-eol-version
49
+ if-no-files-found: error
50
+
51
+ evaluate:
52
+ name: evaluate isolated migrations
53
+ needs: plan
54
+ runs-on: ubuntu-latest
55
+ permissions:
56
+ contents: read
57
+ steps:
58
+ # Provider credentials exist only in this read-only job. The evaluator
59
+ # forwards only names explicitly allowed by eval.pass_env.
60
+ - uses: actions/checkout@v7
61
+ with:
62
+ persist-credentials: false
63
+ - uses: actions/setup-node@v7
64
+ with:
65
+ node-version: 22
66
+ package-manager-cache: false
67
+ - name: Download trusted plan
68
+ uses: actions/download-artifact@v8
69
+ with:
70
+ name: model-eol-plan
71
+ path: ${{ runner.temp }}/model-eol-plan
72
+ - name: Apply and evaluate each migration independently
36
73
  env:
37
74
  OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
38
75
  ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
76
+ GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
77
+ GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
78
+ # A non-empty repository variable overrides eval.command. Otherwise
79
+ # the strictly validated command in .model-eol.json is used.
39
80
  MODEL_EOL_EVAL_COMMAND: ${{ vars.MODEL_EOL_EVAL_COMMAND }}
40
- MODEL_EOL_PLAN: ${{ github.workspace }}/model-eol-plan.json
41
- MODEL_EOL_REPORT: ${{ github.workspace }}/model-eol-report.md
42
81
  run: |
43
- # The workflow variable is an explicit override. Otherwise, use the
44
- # eval.command already validated from .model-eol.json by the plan step.
45
- if [ -z "${MODEL_EOL_EVAL_COMMAND:-}" ] && [ -f .model-eol.json ]; then
46
- MODEL_EOL_EVAL_COMMAND="$(node --input-type=module -e '
47
- import fs from "node:fs"
48
- const config = JSON.parse(fs.readFileSync(".model-eol.json", "utf8"))
49
- if (typeof config.eval?.command === "string") process.stdout.write(config.eval.command)
50
- ')"
82
+ mkdir -p "$RUNNER_TEMP/model-eol-eval"
83
+ MODEL_EOL_VERSION="$(cat "$RUNNER_TEMP/model-eol-plan/model-eol-version")"
84
+ if [[ ! "$MODEL_EOL_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
85
+ echo "refusing invalid resolved model-eol version: $MODEL_EOL_VERSION" >&2
86
+ exit 1
51
87
  fi
52
- # Supply a provider-backed harness for this repository. It may read the
53
- # plan and write Markdown to MODEL_EOL_REPORT, but this job has read-only
54
- # contents permission and the publish job receives no provider keys.
55
- if [ -n "${MODEL_EOL_EVAL_COMMAND:-}" ]; then
56
- set +e
57
- sh -c "$MODEL_EOL_EVAL_COMMAND"
58
- eval_status=$?
59
- set -e
60
- printf '%s\n' "$eval_status" > model-eol-eval-exit-code
61
- else
62
- : > "$MODEL_EOL_REPORT"
63
- printf '0\n' > model-eol-eval-exit-code
64
- fi
65
- - name: Upload plan and eval report
66
- uses: actions/upload-artifact@v4
88
+ npx --yes --package="model-eol@$MODEL_EOL_VERSION" model-eol-bot evaluate \
89
+ --target-dir . \
90
+ --plan-file "$RUNNER_TEMP/model-eol-plan/model-eol-plan.json" \
91
+ --output-file "$RUNNER_TEMP/model-eol-eval/model-eol-eval-results.json"
92
+ - name: Upload bounded eval results
93
+ uses: actions/upload-artifact@v7
67
94
  with:
68
- name: model-eol-plan-and-report
69
- path: |
70
- model-eol-plan.json
71
- model-eol-report.md
72
- model-eol-eval-exit-code
95
+ name: model-eol-eval-results
96
+ path: ${{ runner.temp }}/model-eol-eval/model-eol-eval-results.json
97
+ if-no-files-found: error
73
98
 
74
99
  publish:
75
- name: publish migration work
76
- needs: plan
100
+ name: reconcile migration work
101
+ needs: [plan, evaluate]
77
102
  runs-on: ubuntu-latest
78
103
  permissions:
79
104
  contents: write
80
105
  pull-requests: write
81
106
  issues: write
82
107
  steps:
83
- - uses: actions/checkout@v4
108
+ - uses: actions/checkout@v7
84
109
  with:
85
110
  # model-eol-bot authenticates its own temporary HTTPS clones and
86
111
  # pushes with GITHUB_TOKEN; no checkout credential is persisted.
87
112
  persist-credentials: false
88
- - uses: actions/setup-node@v4
113
+ - uses: actions/setup-node@v7
114
+ with:
115
+ node-version: 22
116
+ package-manager-cache: false
117
+ - name: Download trusted plan
118
+ uses: actions/download-artifact@v8
89
119
  with:
90
- node-version: 20
91
- - name: Download the plan and eval report
92
- uses: actions/download-artifact@v4
120
+ name: model-eol-plan
121
+ path: ${{ runner.temp }}/model-eol-plan
122
+ - name: Download eval results
123
+ uses: actions/download-artifact@v8
93
124
  with:
94
- name: model-eol-plan-and-report
95
- - name: Publish model-eol PRs and issues
125
+ name: model-eol-eval-results
126
+ path: ${{ runner.temp }}/model-eol-eval
127
+ - name: Publish and reconcile model-eol work
96
128
  env:
97
129
  GITHUB_TOKEN: ${{ secrets.MODEL_EOL_BOT_TOKEN || secrets.GITHUB_TOKEN }}
98
- MODEL_EOL_PLAN_FILE: ${{ github.workspace }}/model-eol-plan.json
99
- MODEL_EOL_EVAL_REPORT_FILE: ${{ github.workspace }}/model-eol-report.md
100
- MODEL_EOL_EVAL_STATUS_FILE: ${{ github.workspace }}/model-eol-eval-exit-code
130
+ MODEL_EOL_PLAN_FILE: ${{ runner.temp }}/model-eol-plan/model-eol-plan.json
131
+ MODEL_EOL_EVAL_RESULTS_FILE: ${{ runner.temp }}/model-eol-eval/model-eol-eval-results.json
132
+ # The write job receives the non-secret command text only so it can
133
+ # verify the eval configuration digest. It receives no provider keys.
134
+ MODEL_EOL_EVAL_COMMAND: ${{ vars.MODEL_EOL_EVAL_COMMAND }}
101
135
  MODEL_EOL_TOKEN_KIND: ${{ secrets.MODEL_EOL_BOT_TOKEN && 'external-token' || 'github-token' }}
102
- run: npx --yes --package="$MODEL_EOL_PACKAGE" model-eol-bot --repo "${{ github.repository }}" --target-dir .
136
+ run: |
137
+ MODEL_EOL_VERSION="$(cat "$RUNNER_TEMP/model-eol-plan/model-eol-version")"
138
+ if [[ ! "$MODEL_EOL_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
139
+ echo "refusing invalid resolved model-eol version: $MODEL_EOL_VERSION" >&2
140
+ exit 1
141
+ fi
142
+ npx --yes --package="model-eol@$MODEL_EOL_VERSION" model-eol-bot --repo "${{ github.repository }}" --target-dir .
103
143
 
104
- # Security and checks note:
105
- # A PR made with GITHUB_TOKEN does not trigger pull_request workflows, so checks
106
- # may be skipped even though this job has opened the PR. For required checks, use
107
- # a narrowly scoped fine-grained PAT or a GitHub App installation token in the
108
- # optional MODEL_EOL_BOT_TOKEN secret instead.
109
- # Keep provider API keys only in the read-only plan/eval job and never pass them to
110
- # the publish job.
111
- # Set feeds.allow_vendored_fallback: true in .model-eol.json only for degraded report-only runs; the default is false.
112
- # Independent concurrent invocations need external mutual exclusion; this tool does not lock.
144
+ # A PR made with GITHUB_TOKEN does not trigger pull_request workflows. For
145
+ # required checks, store a narrowly scoped fine-grained PAT or GitHub App token
146
+ # as MODEL_EOL_BOT_TOKEN. Keep provider keys out of the publish job.
147
+ # Set feeds.allow_vendored_fallback: true only for degraded report-only runs.
148
+ # Independent concurrent invocations need external mutual exclusion.
package/check.mjs CHANGED
@@ -19,8 +19,18 @@ import path from 'node:path'
19
19
 
20
20
  import { applyPlan } from './lib/apply.mjs'
21
21
  import { parseCliArgs } from './lib/cli.mjs'
22
- import { CLI_DEFAULT_CONFIG, DEFAULT_CONFIG, loadConfig, normalizeConfig } from './lib/config.mjs'
23
- import { findingFromRef, isBad, loadFeeds } from './lib/feeds.mjs'
22
+ import {
23
+ CLI_DEFAULT_CONFIG,
24
+ DEFAULT_CONFIG,
25
+ configForRepoPath,
26
+ configuredVias,
27
+ loadConfig,
28
+ mappedRoutesForRepoPath,
29
+ normalizeConfig,
30
+ routeForRepoReference,
31
+ } from './lib/config.mjs'
32
+ import { BUILTIN_CHANNELS, findingFromRef, isBad, loadFeeds } from './lib/feeds.mjs'
33
+ import { normalizeRepoPath } from './lib/glob.mjs'
24
34
  import { buildPlan } from './lib/plan.mjs'
25
35
  import {
26
36
  buildAlert,
@@ -73,6 +83,15 @@ const configForTargets = ({ targets, explicit }) => {
73
83
  return { file: files[0] ?? null, roots }
74
84
  }
75
85
 
86
+ const repoPathForFile = (file, roots) => {
87
+ const absolute = path.resolve(file)
88
+ const candidates = roots
89
+ .map(root => ({ root, relative: path.relative(root, absolute) }))
90
+ .filter(({ relative }) => relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative))
91
+ .sort((a, b) => b.root.length - a.root.length)
92
+ return normalizeRepoPath(candidates[0]?.relative ?? absolute)
93
+ }
94
+
76
95
  const main = () => {
77
96
  const COMMANDS = new Set(['check', 'inventory', 'schedule', 'alert', 'plan', 'apply', 'help'])
78
97
  const args = process.argv.slice(2)
@@ -222,6 +241,37 @@ if (feedData.entries.size === 0) {
222
241
  return 2
223
242
  }
224
243
 
244
+ const allowedVias = new Set([...BUILTIN_CHANNELS, ...feedData.vias])
245
+ const unknownVia = [values.via, ...configuredVias(repositoryConfig)]
246
+ .find(via => via && !allowedVias.has(via))
247
+ if (unknownVia) {
248
+ console.error(`unknown lifecycle channel "${unknownVia}"; known values: ${[...allowedVias].sort().join(', ')}`)
249
+ return 2
250
+ }
251
+ const unresolvedRoute = repositoryConfig.routes.find(route => route.model && !feedData.entries.has(route.model))
252
+ if (unresolvedRoute) {
253
+ console.error(`configured route model "${unresolvedRoute.model}" is not present in loaded feeds`)
254
+ return 2
255
+ }
256
+
257
+ const policyCache = new Map()
258
+ const policyForFile = file => {
259
+ const absolute = path.resolve(file)
260
+ if (!policyCache.has(absolute)) {
261
+ const repoPath = repoPathForFile(absolute, configLocation.roots)
262
+ policyCache.set(absolute, { ...configForRepoPath(repositoryConfig, repoPath), repoPath })
263
+ }
264
+ return policyCache.get(absolute)
265
+ }
266
+ const routeForReference = ({ file, matched, id }) => {
267
+ const policy = policyForFile(file)
268
+ return routeForRepoReference(repositoryConfig, policy.repoPath, { matched, id })
269
+ }
270
+ const mappedRoutesForFile = file => {
271
+ const policy = policyForFile(file)
272
+ return mappedRoutesForRepoPath(repositoryConfig, policy.repoPath)
273
+ }
274
+
225
275
  let scan
226
276
  try {
227
277
  scan = scanTargets({
@@ -229,10 +279,11 @@ try {
229
279
  entries: feedData.entries,
230
280
  keys: feedData.keys,
231
281
  includeDocs: INCLUDE_DOCS,
232
- ignoreModels: repositoryConfig.ignore.models,
233
- ignorePaths: repositoryConfig.ignore.paths,
234
282
  ignoreRoots: configLocation.roots,
235
283
  ignoredFiles: configLocation.file ? [configLocation.file] : [],
284
+ policyForFile,
285
+ routeForReference,
286
+ mappedRoutesForFile,
236
287
  })
237
288
  } catch (e) {
238
289
  console.error(`scan failed: ${e.message}`)
@@ -250,10 +301,32 @@ if (incompleteNotes.length && (command === 'check' || command === 'plan')) {
250
301
  if (!ALLOW_INCOMPLETE) return 2
251
302
  }
252
303
 
253
- const findings = scan.modelRefs.map(ref => findingFromRef(ref, { days: DAYS, via: VIA }))
254
- const checkFindings = SCOPE === 'direct'
255
- ? findings.filter(f => f.usage === 'direct-api' || f.usage === 'model-reference')
256
- : findings
304
+ const findings = scan.modelRefs.map(ref => {
305
+ const policy = policyForFile(ref.file)
306
+ const route = routeForReference({ file: ref.file, matched: ref.matched, id: ref.id })
307
+ const days = values.days === undefined ? policy.days : DAYS
308
+ const scope = values.scope === undefined ? policy.scope : SCOPE
309
+ const via = values.via === undefined ? (route?.via ?? policy.via) : VIA
310
+ const finding = findingFromRef(ref, { days, via })
311
+ finding.effective_scope = scope
312
+ finding.requested_via = via
313
+ if (policy.override_indexes.length || route) {
314
+ finding.policy_provenance = {
315
+ override_indexes: policy.override_indexes,
316
+ route_index: route?.index ?? null,
317
+ }
318
+ }
319
+ return finding
320
+ })
321
+ for (const candidate of scan.candidateRefs) {
322
+ const policy = policyForFile(candidate.file)
323
+ candidate.effective_scope = values.scope === undefined ? policy.scope : SCOPE
324
+ if (policy.override_indexes.length) {
325
+ candidate.policy_provenance = { override_indexes: policy.override_indexes, route_index: null }
326
+ }
327
+ }
328
+ const checkFindings = findings.filter(finding =>
329
+ finding.effective_scope !== 'direct' || finding.usage === 'direct-api' || finding.usage === 'model-reference')
257
330
  let changedFindings = checkFindings
258
331
  if (CHANGED_BASE !== null) {
259
332
  try {
package/feeds/amazon.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "spec": "model-eol/0.1",
3
3
  "publisher": "amazon",
4
- "generated": "2026-08-03T21:43:59.800Z",
4
+ "generated": "2026-08-18T00:47:44.931Z",
5
5
  "source": "https://docs.aws.amazon.com/bedrock/latest/userguide/model-lifecycle.html",
6
6
  "models": [
7
7
  {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "spec": "model-eol/0.1",
3
3
  "publisher": "anthropic",
4
- "generated": "2026-08-03T21:43:59.800Z",
4
+ "generated": "2026-08-18T00:47:44.931Z",
5
5
  "source": "https://platform.claude.com/docs/en/about-claude/model-deprecations",
6
6
  "policy": {
7
7
  "min_notice_days": 60,
package/feeds/google.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "spec": "model-eol/0.1",
3
3
  "publisher": "google",
4
- "generated": "2026-08-03T21:43:59.800Z",
4
+ "generated": "2026-08-18T00:47:44.931Z",
5
5
  "source": "https://ai.google.dev/gemini-api/docs/deprecations",
6
6
  "models": [
7
7
  {
@@ -318,7 +318,13 @@
318
318
  },
319
319
  {
320
320
  "id": "gemini-embedding-2",
321
- "source": "https://ai.google.dev/gemini-api/docs/deprecations"
321
+ "source": "https://ai.google.dev/gemini-api/docs/deprecations",
322
+ "distributions": [
323
+ {
324
+ "via": "vertex-ai",
325
+ "source": "https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/model-versions"
326
+ }
327
+ ]
322
328
  },
323
329
  {
324
330
  "id": "gemini-embedding-001",
@@ -518,9 +524,7 @@
518
524
  "source": "https://ai.google.dev/gemini-api/docs/deprecations",
519
525
  "shutdown": "2026-08-31",
520
526
  "date_precision": "earliest",
521
- "replacement_options": [
522
- "gemini-robotics-er-2-preview"
523
- ]
527
+ "replacement": "gemini-robotics-er-2-preview"
524
528
  },
525
529
  {
526
530
  "id": "gemini-robotics-er-1.5-preview",
@@ -528,6 +532,85 @@
528
532
  "shutdown": "2026-04-30",
529
533
  "date_precision": "earliest",
530
534
  "replacement": "gemini-robotics-er-1.6-preview"
535
+ },
536
+ {
537
+ "id": "gemini-3.7-flash",
538
+ "source": "https://ai.google.dev/gemini-api/docs/deprecations",
539
+ "distributions": [
540
+ {
541
+ "via": "vertex-ai",
542
+ "source": "https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/model-versions"
543
+ }
544
+ ]
545
+ },
546
+ {
547
+ "id": "gemma-4-26b-a4b-it"
548
+ },
549
+ {
550
+ "id": "gemma-4-31b-it"
551
+ },
552
+ {
553
+ "id": "gemini-flash-latest"
554
+ },
555
+ {
556
+ "id": "gemini-flash-lite-latest"
557
+ },
558
+ {
559
+ "id": "gemini-pro-latest"
560
+ },
561
+ {
562
+ "id": "gemini-3.1-pro-preview-customtools"
563
+ },
564
+ {
565
+ "id": "nano-banana-pro-preview"
566
+ },
567
+ {
568
+ "id": "gemini-3.1-flash-lite-image",
569
+ "distributions": [
570
+ {
571
+ "via": "vertex-ai",
572
+ "source": "https://docs.cloud.google.com/gemini-enterprise-agent-platform/models/model-versions"
573
+ }
574
+ ]
575
+ },
576
+ {
577
+ "id": "gemini-omni-flash-preview"
578
+ },
579
+ {
580
+ "id": "gemini-robotics-er-2-preview"
581
+ },
582
+ {
583
+ "id": "gemini-2.5-computer-use-preview-10-2025"
584
+ },
585
+ {
586
+ "id": "antigravity-preview-05-2026"
587
+ },
588
+ {
589
+ "id": "deep-research-max-preview-04-2026"
590
+ },
591
+ {
592
+ "id": "deep-research-preview-04-2026"
593
+ },
594
+ {
595
+ "id": "deep-research-pro-preview-12-2025"
596
+ },
597
+ {
598
+ "id": "gemini-embedding-2-preview"
599
+ },
600
+ {
601
+ "id": "aqa"
602
+ },
603
+ {
604
+ "id": "gemini-2.5-flash-native-audio-latest"
605
+ },
606
+ {
607
+ "id": "gemini-2.5-flash-native-audio-preview-09-2025"
608
+ },
609
+ {
610
+ "id": "gemini-robotics-er-2-streaming-preview"
611
+ },
612
+ {
613
+ "id": "gemini-3.5-live-translate-preview"
531
614
  }
532
615
  ]
533
616
  }