model-eol 0.3.0 → 0.4.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.
@@ -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,12 +15,11 @@ 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
23
  - uses: actions/checkout@v4
25
24
  with:
26
25
  persist-credentials: false
@@ -29,51 +28,75 @@ jobs:
29
28
  node-version: 20
30
29
  - name: Build the versioned plan
31
30
  run: |
31
+ mkdir -p "$RUNNER_TEMP/model-eol-plan"
32
+ 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)})')"
33
+ if [[ ! "$MODEL_EOL_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
34
+ echo "refusing invalid resolved model-eol version: $MODEL_EOL_VERSION" >&2
35
+ exit 1
36
+ fi
37
+ printf '%s\n' "$MODEL_EOL_VERSION" > "$RUNNER_TEMP/model-eol-plan/model-eol-version"
32
38
  PLAN_ARGS=(plan .)
33
39
  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
40
+ npx --yes --package="model-eol@$MODEL_EOL_VERSION" model-eol "${PLAN_ARGS[@]}" > "$RUNNER_TEMP/model-eol-plan/model-eol-plan.json"
41
+ - name: Upload plan
42
+ uses: actions/upload-artifact@v4
43
+ with:
44
+ name: model-eol-plan
45
+ path: |
46
+ ${{ runner.temp }}/model-eol-plan/model-eol-plan.json
47
+ ${{ runner.temp }}/model-eol-plan/model-eol-version
48
+ if-no-files-found: error
49
+
50
+ evaluate:
51
+ name: evaluate isolated migrations
52
+ needs: plan
53
+ runs-on: ubuntu-latest
54
+ permissions:
55
+ contents: read
56
+ steps:
57
+ # Provider credentials exist only in this read-only job. The evaluator
58
+ # forwards only names explicitly allowed by eval.pass_env.
59
+ - uses: actions/checkout@v4
60
+ with:
61
+ persist-credentials: false
62
+ - uses: actions/setup-node@v4
63
+ with:
64
+ node-version: 20
65
+ - name: Download trusted plan
66
+ uses: actions/download-artifact@v4
67
+ with:
68
+ name: model-eol-plan
69
+ path: ${{ runner.temp }}/model-eol-plan
70
+ - name: Apply and evaluate each migration independently
36
71
  env:
37
72
  OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
38
73
  ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
74
+ GOOGLE_API_KEY: ${{ secrets.GOOGLE_API_KEY }}
75
+ GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
76
+ # A non-empty repository variable overrides eval.command. Otherwise
77
+ # the strictly validated command in .model-eol.json is used.
39
78
  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
79
  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
- ')"
51
- 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
80
+ mkdir -p "$RUNNER_TEMP/model-eol-eval"
81
+ MODEL_EOL_VERSION="$(cat "$RUNNER_TEMP/model-eol-plan/model-eol-version")"
82
+ if [[ ! "$MODEL_EOL_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
83
+ echo "refusing invalid resolved model-eol version: $MODEL_EOL_VERSION" >&2
84
+ exit 1
64
85
  fi
65
- - name: Upload plan and eval report
86
+ npx --yes --package="model-eol@$MODEL_EOL_VERSION" model-eol-bot evaluate \
87
+ --target-dir . \
88
+ --plan-file "$RUNNER_TEMP/model-eol-plan/model-eol-plan.json" \
89
+ --output-file "$RUNNER_TEMP/model-eol-eval/model-eol-eval-results.json"
90
+ - name: Upload bounded eval results
66
91
  uses: actions/upload-artifact@v4
67
92
  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
93
+ name: model-eol-eval-results
94
+ path: ${{ runner.temp }}/model-eol-eval/model-eol-eval-results.json
95
+ if-no-files-found: error
73
96
 
74
97
  publish:
75
- name: publish migration work
76
- needs: plan
98
+ name: reconcile migration work
99
+ needs: [plan, evaluate]
77
100
  runs-on: ubuntu-latest
78
101
  permissions:
79
102
  contents: write
@@ -88,25 +111,35 @@ jobs:
88
111
  - uses: actions/setup-node@v4
89
112
  with:
90
113
  node-version: 20
91
- - name: Download the plan and eval report
114
+ - name: Download trusted plan
115
+ uses: actions/download-artifact@v4
116
+ with:
117
+ name: model-eol-plan
118
+ path: ${{ runner.temp }}/model-eol-plan
119
+ - name: Download eval results
92
120
  uses: actions/download-artifact@v4
93
121
  with:
94
- name: model-eol-plan-and-report
95
- - name: Publish model-eol PRs and issues
122
+ name: model-eol-eval-results
123
+ path: ${{ runner.temp }}/model-eol-eval
124
+ - name: Publish and reconcile model-eol work
96
125
  env:
97
126
  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
127
+ MODEL_EOL_PLAN_FILE: ${{ runner.temp }}/model-eol-plan/model-eol-plan.json
128
+ MODEL_EOL_EVAL_RESULTS_FILE: ${{ runner.temp }}/model-eol-eval/model-eol-eval-results.json
129
+ # The write job receives the non-secret command text only so it can
130
+ # verify the eval configuration digest. It receives no provider keys.
131
+ MODEL_EOL_EVAL_COMMAND: ${{ vars.MODEL_EOL_EVAL_COMMAND }}
101
132
  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 .
133
+ run: |
134
+ MODEL_EOL_VERSION="$(cat "$RUNNER_TEMP/model-eol-plan/model-eol-version")"
135
+ if [[ ! "$MODEL_EOL_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
136
+ echo "refusing invalid resolved model-eol version: $MODEL_EOL_VERSION" >&2
137
+ exit 1
138
+ fi
139
+ npx --yes --package="model-eol@$MODEL_EOL_VERSION" model-eol-bot --repo "${{ github.repository }}" --target-dir .
103
140
 
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.
141
+ # A PR made with GITHUB_TOKEN does not trigger pull_request workflows. For
142
+ # required checks, store a narrowly scoped fine-grained PAT or GitHub App token
143
+ # as MODEL_EOL_BOT_TOKEN. Keep provider keys out of the publish job.
144
+ # Set feeds.allow_vendored_fallback: true only for degraded report-only runs.
145
+ # 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
  }