Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions content/copilot/reference/ai-models/model-hosting.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ Used for:
* {% data variables.copilot.copilot_claude_opus_46 %}
* {% data variables.copilot.copilot_claude_opus_46_fast %}
* {% data variables.copilot.copilot_claude_opus_47 %}
* {% data variables.copilot.copilot_claude_opus_48 %}
* {% data variables.copilot.copilot_claude_sonnet_46 %}

These models are hosted by Amazon Web Services, Anthropic PBC, and Google Cloud Platform. {% data variables.product.github %} has provider agreements in place to ensure data is not used for training. Additional details for each provider are included below:
Expand Down
19 changes: 10 additions & 9 deletions content/copilot/reference/ai-models/supported-models.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,15 +94,16 @@
{% rowheaders %}

| Model | {% data variables.product.prodname_vscode %} | {% data variables.product.prodname_vs %} | JetBrains IDEs | Xcode | Eclipse |
|------------------------------------------------------| --- | --- | --- | --- | --- |
| {% data variables.copilot.copilot_gemini_3_flash %} | `v1.115.0` and later | `17.14.22` or `18.1.0` and later | `1.5.62` and later | `0.46.0` and later | `0.14.0` and later |
| {% data variables.copilot.copilot_gemini_31_pro %} | `v1.115.0` and later | `17.14.22` or `18.1.0` and later | `1.5.62` and later | `0.46.0` and later | `0.14.0` and later |
| {% data variables.copilot.copilot_gemini_35_flash %} | `v1.115.0` and later | `17.14.22` or `18.1.0` and later | `1.5.62` and later | `0.46.0` and later | `0.14.0` and later |
| {% data variables.copilot.copilot_gpt_52_codex %} | No minimum listed | `17.14.19` or `18.0.0` and later | `1.5.61` and later | `0.45.0` and later | `0.13.0` and later |
| {% data variables.copilot.copilot_gpt_53_codex %} | `v1.104.1` and later | `17.14.19` and later | `1.5.61` and later | `0.45.0` and later | `0.13.0` and later |
| {% data variables.copilot.copilot_gpt_54 %} | `v1.104.1` and later | `17.14.19` and later | `1.5.66` and later | `0.47.0` and later | `0.15.0` and later |
| {% data variables.copilot.copilot_gpt_54_mini %} | `v1.104.1` and later | `17.14.19` and later | `1.5.66` and later | `0.47.0` and later | `0.15.0` and later |
| {% data variables.copilot.copilot_gpt_55 %} | `v1.117` and later | `17.14.19` and later | `1.5.66` and later | `0.47.0` and later | `0.15.0` and later |
|------------------------------------------------------|----------------------------------------------|------------------------------------------| --- | --- | --- |
| {% data variables.copilot.copilot_gemini_3_flash %} | `v1.115.0` and later | `17.14.22` or `18.1.0` and later | `1.5.62` and later | `0.46.0` and later | `0.14.0` and later |
| {% data variables.copilot.copilot_gemini_31_pro %} | `v1.115.0` and later | `17.14.22` or `18.1.0` and later | `1.5.62` and later | `0.46.0` and later | `0.14.0` and later |
| {% data variables.copilot.copilot_gemini_35_flash %} | `v1.115.0` and later | `17.14.22` or `18.1.0` and later | `1.5.62` and later | `0.46.0` and later | `0.14.0` and later |
| {% data variables.copilot.copilot_gpt_52_codex %} | No minimum listed | `17.14.19` or `18.0.0` and later | `1.5.61` and later | `0.45.0` and later | `0.13.0` and later |
| {% data variables.copilot.copilot_gpt_53_codex %} | `v1.104.1` and later | `17.14.19` and later | `1.5.61` and later | `0.45.0` and later | `0.13.0` and later |
| {% data variables.copilot.copilot_gpt_54 %} | `v1.104.1` and later | `17.14.19` and later | `1.5.66` and later | `0.47.0` and later | `0.15.0` and later |
| {% data variables.copilot.copilot_gpt_54_mini %} | `v1.104.1` and later | `17.14.19` and later | `1.5.66` and later | `0.47.0` and later | `0.15.0` and later |
| {% data variables.copilot.copilot_gpt_55 %} | `v1.117` and later | `17.14.19` and later | `1.5.66` and later | `0.47.0` and later | `0.15.0` and later |
| {% data variables.copilot.copilot_claude_opus_48 %} | `v1.118` and later | `17.14.19` and later | TBD | TBD | TBD |

{% endrowheaders %}

Expand All @@ -122,7 +123,7 @@

## Model multipliers

<!-- expires 2026-06-01 -->

Check warning on line 126 in content/copilot/reference/ai-models/supported-models.md

View workflow job for this annotation

GitHub Actions / lint-content

Content that expires soon should be proactively addressed.

Content marked with an expiration date will expire soon. The content exists between 2 HTML comment tags in the format <!-- expires yyyy-mm-dd --> and <!-- end expires yyyy-mm-dd -->. Check whether this content can be removed or rewritten before it expires.

{% data reusables.copilot.ubb-announcement-cfi-cb-ce %}

Expand Down
2 changes: 2 additions & 0 deletions data/reusables/copilot/copilot-cloud-agent-non-auto-models.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
* {% data variables.copilot.copilot_claude_opus_47 %}
* {% data variables.copilot.copilot_claude_haiku_45 %}
* {% data variables.copilot.copilot_gemini_31_pro %}
* {% data variables.copilot.copilot_gemini_35_flash %}
* {% data variables.copilot.copilot_gpt_52_codex %}
* {% data variables.copilot.copilot_gpt_54_mini %}
4 changes: 4 additions & 0 deletions data/tables/copilot/annual-subscriber-model-multipliers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@
current_multiplier: '15'
new_multiplier: '27'

- model: 'Claude Opus 4.8'
current_multiplier: '15'
new_multiplier: '27'

- model: 'Claude Sonnet 4.5'
current_multiplier: '1'
new_multiplier: '6'
Expand Down
5 changes: 5 additions & 0 deletions data/tables/copilot/model-comparison.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@
excels_at: Complex problem-solving challenges, sophisticated reasoning
further_reading: '[Claude Opus 4.7 model card](https://cdn.sanity.io/files/4zrzovbb/website/037f06850df7fbe871e206dad004c3db5fd50340.pdf)'

- name: Claude Opus 4.8
task_area: Deep reasoning and debugging
excels_at: Complex problem-solving challenges, sophisticated reasoning
further_reading: 'Not available'

- name: Claude Sonnet 4.5
task_area: General-purpose coding and agent tasks
excels_at: Complex problem-solving challenges, sophisticated reasoning
Expand Down
4 changes: 4 additions & 0 deletions data/tables/copilot/model-multipliers.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@
multiplier_paid: 15
multiplier_free: Not applicable

- name: Claude Opus 4.8
multiplier_paid: 15
multiplier_free: Not applicable

- name: Claude Sonnet 4.5
multiplier_paid: 1
multiplier_free: Not applicable
Expand Down
7 changes: 7 additions & 0 deletions data/tables/copilot/model-release-status.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,13 @@
ask_mode: true
edit_mode: true

- name: 'Claude Opus 4.8'
provider: 'Anthropic'
release_status: 'GA'
agent_mode: true
ask_mode: true
edit_mode: true

- name: 'Claude Sonnet 4.5'
provider: 'Anthropic'
release_status: 'GA'
Expand Down
13 changes: 11 additions & 2 deletions data/tables/copilot/model-supported-clients.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,15 @@
xcode: true
jetbrains: true

- name: Claude Opus 4.8
dotcom: true
cli: true
vscode: true
vs: true
eclipse: true
xcode: true
jetbrains: true

- name: Claude Sonnet 4.5
dotcom: true
cli: true
Expand Down Expand Up @@ -97,7 +106,7 @@

- name: Gemini 3.1 Pro
dotcom: false
cli: false
cli: true
vscode: true
vs: true
eclipse: true
Expand All @@ -106,7 +115,7 @@

- name: Gemini 3.5 Flash
dotcom: false
cli: false
cli: true
vscode: true
vs: true
eclipse: true
Expand Down
8 changes: 8 additions & 0 deletions data/tables/copilot/model-supported-plans.yml
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,14 @@
business: true
enterprise: true

- name: Claude Opus 4.8
free: false
student: false
pro: false
pro_plus: true
business: true
enterprise: true

- name: Claude Sonnet 4.5
free: false
student: false
Expand Down
9 changes: 9 additions & 0 deletions data/tables/copilot/models-and-pricing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,15 @@
output: $25.00
cache_write: $6.25

- model: Claude Opus 4.8
provider: anthropic
release_status: GA
category: Powerful
input: $5.00
cached_input: $0.50
output: $25.00
cache_write: $6.25

# Google
- model: 'Gemini 2.5 Pro[^5]'
provider: google
Expand Down
1 change: 1 addition & 0 deletions data/variables/copilot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ copilot_claude_opus_45: 'Claude Opus 4.5'
copilot_claude_opus_46: 'Claude Opus 4.6'
copilot_claude_opus_46_fast: 'Claude Opus 4.6 (fast mode) (preview)'
copilot_claude_opus_47: 'Claude Opus 4.7'
copilot_claude_opus_48: 'Claude Opus 4.8'
copilot_claude_sonnet: 'Claude Sonnet'
copilot_claude_sonnet_35: 'Claude Sonnet 3.5'
copilot_claude_sonnet_37: 'Claude Sonnet 3.7'
Expand Down
187 changes: 187 additions & 0 deletions src/article-api/tests/release-notes-transformer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import { describe, expect, test, vi } from 'vitest'

import type { Context, GHESReleasePatch, Page } from '@/types'
import {
ReleaseNotesTransformer,
renderReleaseNotesMarkdown,
} from '@/article-api/transformers/release-notes-transformer'

// Mock renderContent so the unit tests don't need fixtures and so we can
// assert that markdownRequested: true is always threaded through. The mock
// returns the input unchanged so we can verify the output shape.
vi.mock('@/content-render/index', () => ({
renderContent: vi.fn(async (template: string, ctx: { markdownRequested?: boolean }) => {
if (!ctx?.markdownRequested) {
throw new Error('renderContent was called without markdownRequested: true')
}
return template
}),
}))

// Mock the release notes loader so the unit tests don't depend on fixtures
// existing on disk. Returns empty data, which makes the "release not found"
// branch in transform() the deterministic outcome.
vi.mock('@/release-notes/middleware/get-release-notes', () => ({
getReleaseNotes: vi.fn(() => ({})),
}))

const makeContext = (overrides: Partial<Context> = {}): Context =>
({
currentVersion: 'enterprise-server@3.21',
currentLanguage: 'en',
...overrides,
}) as Context

const makePage = (overrides: Partial<Page> = {}): Page =>
({
layout: 'release-notes',
title: 'GitHub Enterprise Server 3.21 release notes',
intro: '',
renderProp: async () => '',
...overrides,
}) as unknown as Page

const samplePatch = (): GHESReleasePatch => ({
version: '3.21.0',
patchVersion: '0',
downloadVersion: '3.21.0',
release: '3.21',
date: '2026-05-26',
intro: 'Intro paragraph for this patch.',
sections: {
features: ['A new feature note.'],
bugs: ['A bug was fixed.', 'Another bug was fixed.'],
known_issues: [
{ heading: 'Instance administration', notes: ['Sub note one.', 'Sub note two.'] },
],
security_fixes: ['**CRITICAL**: Something serious.'],
},
})

describe('ReleaseNotesTransformer', () => {
describe('canTransform', () => {
test('matches pages with layout: release-notes', () => {
const transformer = new ReleaseNotesTransformer()
expect(transformer.canTransform(makePage({ layout: 'release-notes' }))).toBe(true)
})

test('does not match pages with other layouts', () => {
const transformer = new ReleaseNotesTransformer()
expect(transformer.canTransform(makePage({ layout: 'default' } as unknown as Page))).toBe(
false,
)
expect(transformer.canTransform(makePage({ layout: undefined } as unknown as Page))).toBe(
false,
)
})
})

describe('transform error paths', () => {
test('throws when currentVersion is missing', async () => {
const transformer = new ReleaseNotesTransformer()
await expect(
transformer.transform(
makePage(),
'/x',
makeContext({ currentVersion: undefined as unknown as string }),
),
).rejects.toThrow(/No currentVersion/)
})

test('throws when plan is not enterprise-server', async () => {
const transformer = new ReleaseNotesTransformer()
await expect(
transformer.transform(
makePage(),
'/x',
makeContext({ currentVersion: 'free-pro-team@latest' }),
),
).rejects.toThrow(/only supports enterprise-server/)
})

test('throws when release is not found', async () => {
const transformer = new ReleaseNotesTransformer()
await expect(
transformer.transform(
makePage(),
'/x',
makeContext({ currentVersion: 'enterprise-server@0.0' }),
),
).rejects.toThrow(/No release notes found/)
})
})
})

describe('renderReleaseNotesMarkdown', () => {
test('builds H1 title and intro', async () => {
const out = await renderReleaseNotesMarkdown('My Title', 'Intro line.', [], makeContext())
expect(out).toMatch(/^# My Title\n\nIntro line\.$/)
})

test('renders H2 per patch and H3 per section with bulleted notes', async () => {
const out = await renderReleaseNotesMarkdown('Title', '', [samplePatch()], makeContext())

expect(out).toContain('## 3.21.0')
expect(out).toContain('**Release date:** 2026-05-26')
expect(out).toContain('Intro paragraph for this patch.')

expect(out).toContain('### Features')
expect(out).toContain('- A new feature note.')

expect(out).toContain('### Bug fixes')
expect(out).toContain('- A bug was fixed.')
expect(out).toContain('- Another bug was fixed.')

expect(out).toContain('### Security fixes')
expect(out).toContain('- **CRITICAL**: Something serious.')
})

test('renders { heading, notes } as a nested bulleted list', async () => {
const out = await renderReleaseNotesMarkdown('Title', '', [samplePatch()], makeContext())

expect(out).toContain('### Known issues')
// Heading is a top-level bullet, sub-notes are nested under it.
expect(out).toContain('- **Instance administration**')
expect(out).toMatch(
/- \*\*Instance administration\*\*\n {2}- Sub note one\.\n {2}- Sub note two\./,
)
})

test('preserves multi-line notes with continuation indent', async () => {
const patch: GHESReleasePatch = {
...samplePatch(),
sections: { features: ['Line one.\n\nLine two.'] },
}
const out = await renderReleaseNotesMarkdown('Title', '', [patch], makeContext())
expect(out).toMatch(/- Line one\.\n\n {2}Line two\./)
})

test('throws on unrecognized note shape', async () => {
const patch: GHESReleasePatch = {
...samplePatch(),
sections: { features: [{ unknown: 'shape' } as unknown as string] },
}
await expect(renderReleaseNotesMarkdown('Title', '', [patch], makeContext())).rejects.toThrow(
/Unrecognized release note shape/,
)
})

test('uses raw section key when label is unknown', async () => {
const patch: GHESReleasePatch = {
...samplePatch(),
sections: { custom_section: ['A note.'] } as unknown as GHESReleasePatch['sections'],
}
const out = await renderReleaseNotesMarkdown('Title', '', [patch], makeContext())
expect(out).toContain('### custom_section')
})

test('skips empty sections', async () => {
const patch: GHESReleasePatch = {
...samplePatch(),
sections: { features: [], bugs: ['A bug.'] },
}
const out = await renderReleaseNotesMarkdown('Title', '', [patch], makeContext())
expect(out).not.toContain('### Features')
expect(out).toContain('### Bug fixes')
})
})
2 changes: 2 additions & 0 deletions src/article-api/transformers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { JourneyLandingTransformer } from './journey-landing-transformer'
import { CategoryLandingTransformer } from './category-landing-transformer'
import { DiscoveryLandingTransformer } from './discovery-landing-transformer'
import { SearchPageTransformer } from './search-page-transformer'
import { ReleaseNotesTransformer } from './release-notes-transformer'
import { ArticleTransformer } from './article-transformer'

/**
Expand All @@ -39,6 +40,7 @@ transformerRegistry.register(new JourneyLandingTransformer())
transformerRegistry.register(new CategoryLandingTransformer())
transformerRegistry.register(new DiscoveryLandingTransformer())
transformerRegistry.register(new SearchPageTransformer())
transformerRegistry.register(new ReleaseNotesTransformer())
// ArticleTransformer is the catch-all — must be registered last.
transformerRegistry.register(new ArticleTransformer())

Expand Down
Loading
Loading