Skip to content

Commit 087700a

Browse files
authored
Merge pull request #44252 from github/repo-sync
Repo sync
2 parents be89850 + ef72ecf commit 087700a

8 files changed

Lines changed: 530 additions & 168 deletions

File tree

content/copilot/concepts/billing/usage-based-billing-for-individuals.md

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,23 @@ Code completions and {% data variables.copilot.next_edit_suggestions %} are **no
4343

4444
## How do {% data variables.product.prodname_ai_credits_short %} work?
4545

46-
Each {% data variables.product.prodname_copilot_short %} individual plan subscription includes a monthly {% data variables.product.prodname_ai_credits_short %} allowance:
46+
Each {% data variables.product.prodname_copilot_short %} individual plan subscription includes a monthly {% data variables.product.prodname_ai_credits_short %} allowance.
4747

48-
| Plan | Total {% data variables.product.prodname_ai_credits_short %} per month |
49-
| --- | --- |
50-
| {% data variables.copilot.copilot_pro_short %} | {% data variables.copilot.ai_credits_per_user_pro %} |
51-
| {% data variables.copilot.copilot_pro_plus_short %} | {% data variables.copilot.ai_credits_per_user_pro_plus %} |
48+
**Base credits** are included with your plan subscription each month. These match with your subscription price and they never change.
49+
50+
Each plan currently also includes a **flex allotment**. This in an additional monthly amount on top of your base credits. The flex allotment is a variable part of your included usage; it is designed to adapt as the economics of AI evolve, including model pricing, new models, and improvements in efficiency.
51+
52+
Your base credits are used first. If you go beyond your base credits, the flex allotment is applied automatically at the same rates across your IDE, {% data variables.product.prodname_dotcom_the_website %}, and the {% data variables.copilot.copilot_cli_short %}. No additional setup is required. Your usage dashboard shows your available allowance and what you've used.
53+
54+
If you use everything included in your plan, you can purchase more and keep working. See [What happens if I exceed my included {% data variables.product.prodname_ai_credits_short %}](#what-happens-if-i-exceed-my-included--data-variablesproductprodname_ai_credits_short-).
55+
56+
| Plan | Price per month | Base credits | Flex allotment | Total monthly {% data variables.product.prodname_ai_credits_short %} |
57+
| --- | --- | --- | --- | --- |
58+
| {% data variables.copilot.copilot_pro_short %} | {% data variables.copilot.cfi_price_per_month %} | {% data variables.copilot.ai_credits_per_user_pro %} | {% data variables.copilot.ai_credits_per_user_pro_flex %} | {% data variables.copilot.ai_credits_per_user_pro_total %} |
59+
| {% data variables.copilot.copilot_pro_plus_short %} | {% data variables.copilot.cpp_price_per_month %} | {% data variables.copilot.ai_credits_per_user_pro_plus %} | {% data variables.copilot.ai_credits_per_user_pro_plus_flex %} | {% data variables.copilot.ai_credits_per_user_pro_plus_total %} |
60+
| {% data variables.copilot.copilot_max_short %} | {% data variables.copilot.cm_price_per_month %} | {% data variables.copilot.ai_credits_per_user_max %} | {% data variables.copilot.ai_credits_per_user_max_flex %} | {% data variables.copilot.ai_credits_per_user_max_total %} |
61+
62+
{% data variables.copilot.copilot_free_short %} will include 2000 code completions per month, an allowance of {% data variables.product.prodname_ai_credits_short %} and {% data variables.copilot.copilot_auto_model_selection_short %}.
5263

5364
## What happens if I exceed my included {% data variables.product.prodname_ai_credits_short %}?
5465

@@ -79,4 +90,4 @@ Note that, starting **June 1, 2026**, {% data variables.copilot.copilot_pro_shor
7990

8091
## Next steps
8192

82-
* For guidance on how to prepare for usage-based billing, see [AUTOTITLE](/copilot/how-tos/manage-and-track-spending/prepare-for-your-move-to-usage-based-billing).
93+
* For guidance on how to prepare for usage-based billing, see [AUTOTITLE](/copilot/how-tos/manage-and-track-spending/prepare-for-your-move-to-usage-based-billing).

data/variables/copilot.yml

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ copilot_free: 'GitHub Copilot Free'
1616
copilot_free_short: 'Copilot Free'
1717
copilot_student: 'GitHub Copilot Student'
1818
copilot_student_short: 'Copilot Student'
19+
copilot_max: 'GitHub Copilot Max'
20+
copilot_max_short: 'Copilot Max'
1921

2022
## Copilot billing
2123
# Price per additional premium request
@@ -24,6 +26,8 @@ additional_premium_requests: '$0.04 USD' # Note that these are also used to bill
2426
cfi_price_per_month: '$10 USD'
2527
# Price per month for Copilot Pro Plus
2628
cpp_price_per_month: '$39 USD'
29+
# Price per month for Copilot Max
30+
cm_price_per_month: '$100 USD'
2731
# Price per month for Copilot Business
2832
cfb_price_per_month: '$19 USD'
2933
# Price per month for Copilot Enterprise
@@ -34,8 +38,15 @@ ai_credits_per_user_business: '1,900'
3438
ai_credits_per_user_enterprise: '3,900'
3539
ai_credits_per_user_business_promo: '3,000'
3640
ai_credits_per_user_enterprise_promo: '7,000'
37-
ai_credits_per_user_pro: '1000'
38-
ai_credits_per_user_pro_plus: '3900'
41+
ai_credits_per_user_pro: '1,000'
42+
ai_credits_per_user_pro_plus: '3,900'
43+
ai_credits_per_user_max: '10,000'
44+
ai_credits_per_user_pro_flex: '500'
45+
ai_credits_per_user_pro_plus_flex: '3,100'
46+
ai_credits_per_user_max_flex: '10,000'
47+
ai_credits_per_user_pro_total: '1,500'
48+
ai_credits_per_user_pro_plus_total: '7,000'
49+
ai_credits_per_user_max_total: '20,000'
3950

4051
## Copilot partners: builders who can develop Copilot extensions
4152
copilot_partners: 'Copilot Partners'

src/graphql/data/ghec/schema.docs.graphql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69470,7 +69470,7 @@ type User implements Actor & Agentic & Node & PackageOwner & ProfileOwner & Proj
6947069470
): Organization
6947169471

6947269472
"""
69473-
Verified email addresses that match verified domains for a specified organization the user is a member of. Results are unordered. There is no way to specify ordering, priority, or filtering, and this field should not be used to determine a user's canonical or current corporate email in multi-domain contexts.
69473+
Verified email addresses that match verified domains for a specified organization the user is a member of.
6947469474
"""
6947569475
organizationVerifiedDomainEmails(
6947669476
"""

src/links/lib/extract-links.ts

Lines changed: 69 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,34 @@ export interface LinkExtractionResult {
5757
}
5858

5959
/**
60-
* Get line and column number for a match in content
60+
* Build an array of character offsets at which each line starts.
61+
* offsets[0] is always 0. Called once per extractLinksFromMarkdown invocation
62+
* so that getLineAndColumn can use binary search instead of repeated splits.
6163
*/
62-
function getLineAndColumn(content: string, matchIndex: number): { line: number; column: number } {
63-
const lines = content.substring(0, matchIndex).split('\n')
64-
const line = lines.length
65-
const column = lines[lines.length - 1].length + 1
66-
return { line, column }
64+
function buildLineOffsets(content: string): number[] {
65+
const offsets = [0]
66+
for (let i = 0; i < content.length; i++) {
67+
if (content[i] === '\n') offsets.push(i + 1)
68+
}
69+
return offsets
70+
}
71+
72+
/**
73+
* Get line and column number for a match using a precomputed line-offset index.
74+
* Binary search gives O(log L) per call instead of O(matchIndex).
75+
*/
76+
function getLineAndColumn(
77+
lineOffsets: number[],
78+
matchIndex: number,
79+
): { line: number; column: number } {
80+
let lo = 0
81+
let hi = lineOffsets.length - 1
82+
while (lo < hi) {
83+
const mid = (lo + hi + 1) >> 1
84+
if (lineOffsets[mid] <= matchIndex) lo = mid
85+
else hi = mid - 1
86+
}
87+
return { line: lo + 1, column: matchIndex - lineOffsets[lo] + 1 }
6788
}
6889

6990
/**
@@ -109,10 +130,13 @@ export function extractLinksFromMarkdown(content: string): LinkExtractionResult
109130
},
110131
)
111132

133+
// Precompute line-start offsets once so every getLineAndColumn call is O(log L).
134+
const lineOffsets = buildLineOffsets(strippedContent)
135+
112136
// Extract AUTOTITLE links first (they're a special case of internal links)
113137
let match
114138
while ((match = AUTOTITLE_LINK_PATTERN.exec(strippedContent)) !== null) {
115-
const { line, column } = getLineAndColumn(strippedContent, match.index)
139+
const { line, column } = getLineAndColumn(lineOffsets, match.index)
116140
const href = match[1].split('#')[0] // Remove anchor if present
117141
if (href.startsWith('/')) {
118142
internalLinks.push({
@@ -136,7 +160,7 @@ export function extractLinksFromMarkdown(content: string): LinkExtractionResult
136160
continue
137161
}
138162

139-
const { line, column } = getLineAndColumn(strippedContent, match.index)
163+
const { line, column } = getLineAndColumn(lineOffsets, match.index)
140164
// Extract href from ](/path) format
141165
const href = fullMatch.substring(2, fullMatch.length - 1).split('#')[0]
142166
const text = extractLinkText(strippedContent, match.index)
@@ -155,7 +179,7 @@ export function extractLinksFromMarkdown(content: string): LinkExtractionResult
155179

156180
// Extract external links
157181
while ((match = EXTERNAL_LINK_PATTERN.exec(strippedContent)) !== null) {
158-
const { line, column } = getLineAndColumn(strippedContent, match.index)
182+
const { line, column } = getLineAndColumn(lineOffsets, match.index)
159183
const href = match[1]
160184
const text = extractLinkText(strippedContent, match.index)
161185

@@ -172,7 +196,7 @@ export function extractLinksFromMarkdown(content: string): LinkExtractionResult
172196

173197
// Extract anchor links
174198
while ((match = ANCHOR_LINK_PATTERN.exec(strippedContent)) !== null) {
175-
const { line, column } = getLineAndColumn(strippedContent, match.index)
199+
const { line, column } = getLineAndColumn(lineOffsets, match.index)
176200
const href = match[0].substring(2, match[0].length - 1)
177201

178202
anchorLinks.push({
@@ -188,7 +212,7 @@ export function extractLinksFromMarkdown(content: string): LinkExtractionResult
188212

189213
// Extract image links
190214
while ((match = IMAGE_LINK_PATTERN.exec(strippedContent)) !== null) {
191-
const { line, column } = getLineAndColumn(strippedContent, match.index)
215+
const { line, column } = getLineAndColumn(lineOffsets, match.index)
192216
const href = match[1]
193217

194218
// Only include internal images (starting with /)
@@ -208,7 +232,7 @@ export function extractLinksFromMarkdown(content: string): LinkExtractionResult
208232
// Extract reference-style link definitions ([id]: /path)
209233
// These are distinct from inline links but point to the same targets that need validating.
210234
while ((match = LINK_DEFINITION_PATTERN.exec(strippedContent)) !== null) {
211-
const { line, column } = getLineAndColumn(strippedContent, match.index)
235+
const { line, column } = getLineAndColumn(lineOffsets, match.index)
212236
const href = match[1].split('#')[0]
213237
internalLinks.push({
214238
href,
@@ -223,7 +247,7 @@ export function extractLinksFromMarkdown(content: string): LinkExtractionResult
223247

224248
// Extract links whose href starts with a Liquid tag
225249
while ((match = LIQUID_HREF_PATTERN.exec(strippedContent)) !== null) {
226-
const { line, column } = getLineAndColumn(strippedContent, match.index)
250+
const { line, column } = getLineAndColumn(lineOffsets, match.index)
227251
liquidPrefixedLinks.push({
228252
href: match[1],
229253
line,
@@ -274,6 +298,18 @@ export function createLiquidContext(
274298
} as Context
275299
}
276300

301+
// Cached reference to renderLiquid — avoids repeated dynamic-import overhead on every call.
302+
// A dynamic import is still used (not a top-level import) to prevent circular dependency issues.
303+
type RenderLiquidModule = (template: string, context: unknown) => Promise<string>
304+
let _renderLiquid: RenderLiquidModule | null = null
305+
async function getCachedRenderLiquid(): Promise<RenderLiquidModule> {
306+
if (!_renderLiquid) {
307+
const mod = await import('@/content-render/liquid/index')
308+
_renderLiquid = mod.renderLiquid
309+
}
310+
return _renderLiquid
311+
}
312+
277313
/**
278314
* Render Liquid templates in content and extract links
279315
*
@@ -285,8 +321,8 @@ export async function extractLinksWithLiquid(
285321
context: Context,
286322
): Promise<LinkExtractionResult> {
287323
try {
288-
// Dynamic import to avoid circular dependency issues
289-
const { renderLiquid } = await import('@/content-render/liquid/index')
324+
// Dynamic import to avoid circular dependency issues (cached after first load)
325+
const renderLiquid = await getCachedRenderLiquid()
290326
// Render Liquid to expand conditionals
291327
const rendered = await renderLiquid(content, context)
292328
return extractLinksFromMarkdown(rendered)
@@ -298,6 +334,24 @@ export async function extractLinksWithLiquid(
298334
}
299335
}
300336

337+
/**
338+
* Render Liquid templates in content, returning both the rendered markdown string and
339+
* extracted links. Use this when both are needed to avoid rendering the same content twice.
340+
*/
341+
export async function renderAndExtractLinks(
342+
content: string,
343+
context: Context,
344+
): Promise<{ renderedMarkdown: string; result: LinkExtractionResult }> {
345+
try {
346+
const renderLiquid = await getCachedRenderLiquid()
347+
const renderedMarkdown = await renderLiquid(content, context)
348+
return { renderedMarkdown, result: extractLinksFromMarkdown(renderedMarkdown) }
349+
} catch (error) {
350+
console.warn('Liquid rendering failed, falling back to raw extraction:', error)
351+
return { renderedMarkdown: content, result: extractLinksFromMarkdown(content) }
352+
}
353+
}
354+
301355
/**
302356
* Read a file and extract links
303357
*/

src/links/lib/link-report.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export interface LinkReport {
3232
title: string
3333
summary: string
3434
groups: GroupedBrokenLinks[]
35+
selfReferentialGroups?: GroupedBrokenLinks[]
3536
uniqueTargets: number
3637
totalOccurrences: number
3738
timestamp: string
@@ -96,6 +97,25 @@ ${statusInfo}${suggestion}**Found in ${count} file${plural}:**
9697
${tableRows}`
9798
},
9899

100+
// Self-referential links section
101+
selfReferentialLinks: (title: string, groups: GroupedBrokenLinks[]) => {
102+
const totalOccurrences = groups.reduce((sum, g) => sum + g.occurrences.length, 0)
103+
const rows = groups
104+
.map((g) => {
105+
const uniqueFileCount = new Set(g.occurrences.map((occ) => occ.file)).size
106+
const occRows = g.occurrences
107+
.map((occ) => `| \`${occ.file}\` | ${occ.lines.join(', ')} |`)
108+
.join('\n')
109+
return `### \`${g.target}\`\n\n**Found in ${uniqueFileCount} file${uniqueFileCount === 1 ? '' : 's'}:**\n\n| File | Line(s) |\n|------|---------|\n${occRows}`
110+
})
111+
.join('\n\n')
112+
return `## 🔗 ${title} (${groups.length} unique URL${groups.length === 1 ? '' : 's'}, ${totalOccurrences} occurrence${totalOccurrences === 1 ? '' : 's'})
113+
114+
The following links point to \`docs.github.com\`. Consider replacing them with relative internal links using the \`[AUTOTITLE](/path/to/article)\` syntax.
115+
116+
${rows}`
117+
},
118+
99119
// Empty report
100120
noIssues: () => 'No issues found! 🎉',
101121

@@ -301,9 +321,12 @@ export function generateInternalLinkReport(
301321
*/
302322
export function generateExternalLinkReport(
303323
brokenLinks: BrokenLink[],
304-
options: { actionUrl?: string } = {},
324+
options: { actionUrl?: string; selfReferentialLinks?: BrokenLink[] } = {},
305325
): LinkReport {
306326
const groups = groupExternalLinksByDomain(brokenLinks)
327+
const selfReferentialGroups = options.selfReferentialLinks?.length
328+
? groupBrokenLinks(options.selfReferentialLinks)
329+
: undefined
307330
const count = groups.length
308331
const plural = count === 1 ? '' : 's'
309332

@@ -314,6 +337,7 @@ export function generateExternalLinkReport(
314337
? `Found **${brokenLinks.length}** broken external link${brokenLinks.length === 1 ? '' : 's'} across **${count}** domain${plural}.`
315338
: 'All external links are valid! ✅',
316339
groups,
340+
selfReferentialGroups,
317341
uniqueTargets: count,
318342
totalOccurrences: brokenLinks.length,
319343
timestamp: new Date().toISOString(),
@@ -360,14 +384,16 @@ function renderGroups(groups: GroupedBrokenLinks[], isExternal: boolean): string
360384
*/
361385
export function reportToMarkdown(report: LinkReport, isExternal = false): string {
362386
const parts: string[] = []
387+
const hasBrokenOrRedirectGroups = report.groups.length > 0
388+
const hasSelfReferentialGroups = Boolean(report.selfReferentialGroups?.length)
363389

364390
// Header
365391
parts.push(
366392
TEMPLATES.reportHeader(report.title, report.summary, report.timestamp, report.actionUrl),
367393
)
368394
parts.push('')
369395

370-
if (report.groups.length === 0) {
396+
if (!hasBrokenOrRedirectGroups && !hasSelfReferentialGroups) {
371397
parts.push(TEMPLATES.noIssues())
372398
return parts.join('\n')
373399
}
@@ -379,7 +405,17 @@ export function reportToMarkdown(report: LinkReport, isExternal = false): string
379405
}
380406

381407
// Groups
382-
parts.push(renderGroups(report.groups, isExternal))
408+
if (hasBrokenOrRedirectGroups) {
409+
parts.push(renderGroups(report.groups, isExternal))
410+
}
411+
412+
// Self-referential links section (external report only)
413+
if (hasSelfReferentialGroups) {
414+
parts.push(
415+
TEMPLATES.selfReferentialLinks('Potential Internal Links', report.selfReferentialGroups!),
416+
)
417+
parts.push('')
418+
}
383419

384420
return parts.join('\n')
385421
}

0 commit comments

Comments
 (0)