diff --git a/.github/workflows/link-check-internal.yml b/.github/workflows/link-check-internal.yml
index 0d675fda80ba..55a738e86400 100644
--- a/.github/workflows/link-check-internal.yml
+++ b/.github/workflows/link-check-internal.yml
@@ -113,7 +113,7 @@ jobs:
- name: Create Copilot redirect issue
if: inputs.create_copilot_issue
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
with:
github-token: ${{ secrets.DOCS_BOT_PAT_BASE }}
script: |
diff --git a/content/admin/data-residency/github-copilot-with-data-residency.md b/content/admin/data-residency/github-copilot-with-data-residency.md
index 004c50122af6..79843a7a7eb6 100644
--- a/content/admin/data-residency/github-copilot-with-data-residency.md
+++ b/content/admin/data-residency/github-copilot-with-data-residency.md
@@ -48,16 +48,17 @@ The models available for {% data variables.product.prodname_copilot_short %} var
### European Union
-* GPT-4o mini
-* GPT-4.1
-* GPT-5 mini
-* GPT-5.2
-* GPT-5.4
-* Claude Haiku 4.5
-* Claude Sonnet 4.5
-* Claude Opus 4.5
-* Claude Sonnet 4.6
-* Claude Opus 4.6
+* {% data variables.copilot.copilot_gpt_4o_mini %}
+* {% data variables.copilot.copilot_gpt_41 %}
+* {% data variables.copilot.copilot_gpt_5_mini %}
+* {% data variables.copilot.copilot_gpt_52 %}
+* {% data variables.copilot.copilot_gpt_53_codex %}
+* {% data variables.copilot.copilot_gpt_54 %}
+* {% data variables.copilot.copilot_claude_haiku_45 %}
+* {% data variables.copilot.copilot_claude_sonnet_45 %}
+* {% data variables.copilot.copilot_claude_opus_45 %}
+* {% data variables.copilot.copilot_claude_sonnet_46 %}
+* {% data variables.copilot.copilot_claude_opus_46 %}
## Pricing changes
diff --git a/content/copilot/concepts/auto-model-selection.md b/content/copilot/concepts/auto-model-selection.md
index bbd044eee70d..1232bf7adbbf 100644
--- a/content/copilot/concepts/auto-model-selection.md
+++ b/content/copilot/concepts/auto-model-selection.md
@@ -18,7 +18,7 @@ Experience less rate limiting and reduce the mental load of choosing a model by
{% data variables.copilot.copilot_auto_model_selection %} intelligently chooses models based on real time system health and model performance. You benefit from:
* Reduced rate limiting
* Lower latency and errors
- * Discounted multipliers for paid plans ({% data variables.copilot.copilot_chat_short %} only)
+ * Discounted multipliers for paid plans
{% data variables.copilot.copilot_auto_model_selection_short_cap_a %} **won't** include these models:
* Models excluded by administrator policies. See [AUTOTITLE](/copilot/how-tos/copilot-on-github/set-up-copilot/configure-access-to-ai-models).
diff --git a/content/copilot/how-tos/administer-copilot/manage-for-enterprise/use-your-own-api-keys.md b/content/copilot/how-tos/administer-copilot/manage-for-enterprise/use-your-own-api-keys.md
index fd1a5127c679..2a2881439468 100644
--- a/content/copilot/how-tos/administer-copilot/manage-for-enterprise/use-your-own-api-keys.md
+++ b/content/copilot/how-tos/administer-copilot/manage-for-enterprise/use-your-own-api-keys.md
@@ -11,7 +11,7 @@ category:
- Manage Copilot for a team
---
-{% data reusables.copilot.byok-intro %}
+{% data reusables.copilot.byok-intro %} {% data reusables.copilot.byok-no-subscription-required %}
## Why bring your own API keys?
@@ -25,6 +25,8 @@ As an enterprise owner, you may have specific requirements for governance, data
After you've added your key and selected one or more models, you and members of your organizations will be able to use them with {% data variables.copilot.copilot_byok_supported_features %}. Your models will appear at the bottom of the model picker, under the enterprise name.
+> [!NOTE] For members of your organizations to use third-party models in {% data variables.product.prodname_vscode %}, the **Bring Your Own Language Model Key in {% data variables.product.prodname_vscode_shortname %}** policy must be enabled. For more information, see the [{% data variables.product.prodname_copilot_short %} settings page](https://github.com/settings/copilot/features) in {% data variables.product.prodname_dotcom_the_website %}.
+
{% data reusables.enterprise-accounts.access-enterprise %}
{% data reusables.enterprise-accounts.ai-controls-tab %}
1. In the sidebar, click **{% octicon "copilot" aria-hidden="true" aria-label="copilot" %} {% data variables.product.prodname_copilot_short %}**.
diff --git a/content/copilot/how-tos/administer-copilot/manage-for-organization/use-your-own-api-keys.md b/content/copilot/how-tos/administer-copilot/manage-for-organization/use-your-own-api-keys.md
index d9484a4d3365..775c470cbf2c 100644
--- a/content/copilot/how-tos/administer-copilot/manage-for-organization/use-your-own-api-keys.md
+++ b/content/copilot/how-tos/administer-copilot/manage-for-organization/use-your-own-api-keys.md
@@ -11,7 +11,7 @@ category:
- Manage Copilot for a team
---
-{% data reusables.copilot.byok-intro %}
+{% data reusables.copilot.byok-intro %} {% data reusables.copilot.byok-no-subscription-required %}
## Why bring your own API keys?
diff --git a/content/copilot/how-tos/copilot-on-github/set-up-copilot/configure-access-to-ai-models.md b/content/copilot/how-tos/copilot-on-github/set-up-copilot/configure-access-to-ai-models.md
index a8d3edb77451..cc88f1f925a3 100644
--- a/content/copilot/how-tos/copilot-on-github/set-up-copilot/configure-access-to-ai-models.md
+++ b/content/copilot/how-tos/copilot-on-github/set-up-copilot/configure-access-to-ai-models.md
@@ -34,7 +34,9 @@ If you have a {% data variables.copilot.copilot_free_short %}, {% data variables
As an enterprise or organization owner, you can enable or disable access to AI models for members with a {% data variables.copilot.copilot_enterprise_short %} or {% data variables.copilot.copilot_business_short %} seat. See [AUTOTITLE](/copilot/managing-copilot/managing-github-copilot-in-your-organization/setting-policies-for-copilot-in-your-organization/managing-policies-for-copilot-in-your-organization) and [AUTOTITLE](/copilot/managing-copilot/managing-copilot-for-your-enterprise/managing-policies-and-features-for-copilot-in-your-enterprise).
-> [!NOTE] Models available in {% data variables.copilot.copilot_auto_model_selection %} will follow the policies set for an organization or enterprise. See [AUTOTITLE](/copilot/concepts/auto-model-selection).
+> [!NOTE]
+> * Models available in {% data variables.copilot.copilot_auto_model_selection %} will follow the policies set for an organization or enterprise. See [AUTOTITLE](/copilot/concepts/auto-model-selection).
+> * {% data reusables.copilot.byok-no-subscription-required %}
{% ifversion copilot-byok %}
diff --git a/content/copilot/how-tos/use-ai-models/change-the-chat-model.md b/content/copilot/how-tos/use-ai-models/change-the-chat-model.md
index 2a1d126dbf9e..ff2674758942 100644
--- a/content/copilot/how-tos/use-ai-models/change-the-chat-model.md
+++ b/content/copilot/how-tos/use-ai-models/change-the-chat-model.md
@@ -9,7 +9,7 @@ redirect_from:
- /copilot/how-tos/ai-models/changing-the-ai-model-for-copilot-chat
- /copilot/how-tos/ai-models/change-the-chat-model
contentType: how-tos
-category:
+category:
- Configure Copilot
---
@@ -75,6 +75,7 @@ You can expand the model options that are available to power {% data variables.c
* Depending on the provider or model you choose, you may need to supply an API key, or model ID, from the provider, or a {% data variables.product.github %} {% data variables.product.pat_generic %} (PAT).
* To add models from the AI Toolkit for {% data variables.product.prodname_vscode %}, you must install the AI Toolkit extension.
+* If you are a {% data variables.copilot.copilot_business_short %} or {% data variables.copilot.copilot_enterprise_short %} customer and want to use third-party models in {% data variables.product.prodname_vscode %}, the **Bring Your Own Language Model Key in {% data variables.product.prodname_vscode_shortname %}** policy must be enabled. For more information, see the [{% data variables.product.prodname_copilot_short %} settings page](https://github.com/settings/copilot/features) in {% data variables.product.prodname_dotcom_the_website %}.
### Adding models
diff --git a/data/reusables/copilot/byok-no-subscription-required.md b/data/reusables/copilot/byok-no-subscription-required.md
new file mode 100644
index 000000000000..debc57271583
--- /dev/null
+++ b/data/reusables/copilot/byok-no-subscription-required.md
@@ -0,0 +1 @@
+Using your own API keys does not require a {% data variables.product.prodname_copilot_short %} subscription. However, without a subscription, you won't have access to other {% data variables.product.prodname_copilot_short %} capabilities such as mobile access, automation, and remote server features.
diff --git a/data/variables/copilot.yml b/data/variables/copilot.yml
index af7b6263a455..f9407ace0ce5 100644
--- a/data/variables/copilot.yml
+++ b/data/variables/copilot.yml
@@ -222,4 +222,4 @@ copilot_workspace: 'Copilot Workspace'
copilot_workspace_short: 'Workspace'
# BYOK
-copilot_byok_supported_features: '{% data variables.copilot.copilot_chat %} and {% data variables.copilot.copilot_cli %}'
+copilot_byok_supported_features: '{% data variables.copilot.copilot_chat_short %}, {% data variables.copilot.copilot_cli_short %}, and {% data variables.product.prodname_vscode_shortname %}'
diff --git a/eslint.config.ts b/eslint.config.ts
index 4c58be15aefa..f6e8541df9e7 100644
--- a/eslint.config.ts
+++ b/eslint.config.ts
@@ -177,7 +177,6 @@ export default [
'src/article-api/**/*.{ts,js}',
'src/audit-logs/**/*.{ts,js}',
'src/color-schemes/**/*.{ts,js}',
- 'src/content-render/**/*.{ts,js}',
'src/data-directory/**/*.{ts,js}',
'src/dev-toc/**/*.{ts,js}',
'src/events/**/*.{ts,js}',
diff --git a/src/content-render/index.ts b/src/content-render/index.ts
index 6591d079c024..ef211ba1b80d 100644
--- a/src/content-render/index.ts
+++ b/src/content-render/index.ts
@@ -2,6 +2,9 @@ import { renderLiquid } from './liquid/index'
import { renderMarkdown, renderUnified } from './unified/index'
import { engine } from './liquid/engine'
import type { Context } from '@/types'
+import { createLogger } from '@/observability/logger'
+
+const logger = createLogger(import.meta.url)
interface RenderOptions {
cache?: boolean | ((template: string, context: Context) => string)
@@ -53,7 +56,7 @@ export async function renderContent(
return html
} catch (error) {
if (options.filename) {
- console.error(`renderContent failed on file: ${options.filename}`)
+ logger.error('renderContent failed on file', { filename: options.filename })
}
throw error
}
diff --git a/src/content-render/liquid/data.ts b/src/content-render/liquid/data.ts
index 28c8f581738f..24f2d566a752 100644
--- a/src/content-render/liquid/data.ts
+++ b/src/content-render/liquid/data.ts
@@ -3,6 +3,9 @@ import type { TagToken, Liquid, Template } from 'liquidjs'
import { THROW_ON_EMPTY, DataReferenceError } from './error-handling'
import { getDataByLanguage } from '@/data-directory/lib/get-data'
+import { createLogger } from '@/observability/logger'
+
+const logger = createLogger(import.meta.url)
const Syntax = /([a-z0-9/\\_.\-[\]]+)/i
const SyntaxHelp = "Syntax Error in 'data' - Valid syntax: data [path]"
@@ -42,7 +45,7 @@ export default {
if (THROW_ON_EMPTY) {
throw new DataReferenceError(message)
}
- console.warn(message)
+ logger.warn(message)
}
return
}
diff --git a/src/content-render/liquid/ifversion.ts b/src/content-render/liquid/ifversion.ts
index 60a4a5cc070f..adc1b16d5ccb 100644
--- a/src/content-render/liquid/ifversion.ts
+++ b/src/content-render/liquid/ifversion.ts
@@ -14,6 +14,9 @@ import versionSatisfiesRange from '@/versions/lib/version-satisfies-range'
import supportedOperators, {
type IfversionSupportedOperator,
} from './ifversion-supported-operators'
+import { createLogger } from '@/observability/logger'
+
+const logger = createLogger(import.meta.url)
interface Branch {
cond: string
@@ -174,17 +177,7 @@ export default class Ifversion extends Tag {
}
if (!this.currentVersionObj) {
- console.warn(
- `
- If this happens, it means the context prepared for rendering Liquid
- did not supply an object called 'currentVersionObj'.
- To fix the error, find the code that prepares the context before
- calling 'liquid.parseAndRender' and make sure there's an object
- called 'currentVersionObj' included there.
- `
- .replace(/\n\s+/g, ' ')
- .trim(),
- )
+ logger.warn('Context missing currentVersionObj for Liquid rendering')
throw new Error('currentVersionObj not found in environment context.')
}
@@ -222,7 +215,7 @@ export default class Ifversion extends Tag {
handleVersionNames(resolvedBranchCond: string): string {
if (!this.currentVersionObj) {
- console.warn('currentVersionObj not found in ifversion context.')
+ logger.warn('currentVersionObj not found in ifversion context')
return resolvedBranchCond
}
diff --git a/src/content-render/liquid/indented-data-reference.ts b/src/content-render/liquid/indented-data-reference.ts
index 2a77920ab2d9..64dac8a43d6a 100644
--- a/src/content-render/liquid/indented-data-reference.ts
+++ b/src/content-render/liquid/indented-data-reference.ts
@@ -3,6 +3,9 @@ import assert from 'assert'
import { type TagToken, type Liquid } from 'liquidjs'
import { THROW_ON_EMPTY, IndentedDataReferenceError } from './error-handling'
import { getDataByLanguage } from '@/data-directory/lib/get-data'
+import { createLogger } from '@/observability/logger'
+
+const logger = createLogger(import.meta.url)
interface LiquidScope {
environments: {
@@ -55,7 +58,7 @@ const IndentedDataReference = {
if (THROW_ON_EMPTY) {
throw new IndentedDataReferenceError(message)
}
- console.warn(message)
+ logger.warn(message)
}
return
}
diff --git a/src/content-render/tests/copilot-code-blocks.ts b/src/content-render/tests/copilot-code-blocks.ts
index 45dbbbf77905..51209f95663e 100644
--- a/src/content-render/tests/copilot-code-blocks.ts
+++ b/src/content-render/tests/copilot-code-blocks.ts
@@ -1,6 +1,16 @@
import { describe, it, expect, vi } from 'vitest'
import { renderContent } from '@/content-render/index'
+const { mockWarn } = vi.hoisted(() => ({ mockWarn: vi.fn() }))
+vi.mock('@/observability/logger', () => ({
+ createLogger: () => ({
+ info: vi.fn(),
+ warn: mockWarn,
+ error: vi.fn(),
+ debug: vi.fn(),
+ }),
+}))
+
describe('code-header plugin', () => {
describe('copilot language code blocks', () => {
it('should render basic copilot code block without header (no copy meta)', async () => {
@@ -126,18 +136,17 @@ Improve the variable names in this function
describe('edge cases', () => {
it('should handle missing reference gracefully and fall back to current code only', async () => {
- // Mock console.warn to capture warning
- const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
+ mockWarn.mockClear()
const markdown =
'```copilot copy prompt ref=nonexistent-id\nImprove the variable names in this function\n```'
const html = await renderContent(markdown)
- // Should warn about missing reference
- expect(consoleWarnSpy).toHaveBeenCalledWith(
- expect.stringContaining("Can't find referenced code block with id=nonexistent-id"),
- )
+ // Should warn about missing reference via structured logger
+ expect(mockWarn).toHaveBeenCalledWith('Cannot find referenced code block', {
+ ref: 'nonexistent-id',
+ })
// Should still render with prompt button using current code only
expect(html).toContain('https://github.com/copilot?prompt=')
@@ -149,8 +158,7 @@ Improve the variable names in this function
// Should not crash or fail
expect(html).toContain('code-example')
- // Restore console.warn
- consoleWarnSpy.mockRestore()
+ mockWarn.mockClear()
})
it('should not process annotated code blocks', async () => {
diff --git a/src/content-render/unified/alerts.ts b/src/content-render/unified/alerts.ts
index 61dc11d9961d..8e6660eee2cb 100644
--- a/src/content-render/unified/alerts.ts
+++ b/src/content-render/unified/alerts.ts
@@ -6,6 +6,9 @@ import { visit } from 'unist-util-visit'
import { h } from 'hastscript'
import octicons from '@primer/octicons'
import type { Element, Root, ElementContent } from 'hast'
+import { createLogger } from '@/observability/logger'
+
+const logger = createLogger(import.meta.url)
interface AlertType {
icon: string
@@ -33,9 +36,7 @@ export default function alerts({ alertTitles = {} }: { alertTitles?: Record' meta is found, find a matching code block to include as context in the prompt link.
const matchingCodeEl = findMatchingCode(ref as string, tree)
if (!matchingCodeEl) {
- console.warn(`Can't find referenced code block with id=${ref}`)
+ logger.warn('Cannot find referenced code block', { ref })
return promptOnly(code)
}
// AST structure: element -> code -> text node with value property
diff --git a/src/content-render/unified/rewrite-asset-urls.ts b/src/content-render/unified/rewrite-asset-urls.ts
index eb112d4ed7ea..76cea7d7658a 100644
--- a/src/content-render/unified/rewrite-asset-urls.ts
+++ b/src/content-render/unified/rewrite-asset-urls.ts
@@ -1,6 +1,9 @@
import fs from 'fs'
import type { Element, Node } from 'hast'
import { visit } from 'unist-util-visit'
+import { createLogger } from '@/observability/logger'
+
+const logger = createLogger(import.meta.url)
// Process-level cache for stat results — file sizes don't change between deploys.
const statCache = new Map()
@@ -59,9 +62,6 @@ function getNewSrc(node: Element): string | undefined {
return split.join('/')
} catch {
statCache.set(filePath, null)
- console.warn(
- `Failed to get a hash for ${src} ` +
- '(This is mostly harmless and can happen with outdated translations).',
- )
+ logger.warn('Failed to get a hash for asset URL', { src })
}
}
diff --git a/src/content-render/unified/rewrite-local-links.ts b/src/content-render/unified/rewrite-local-links.ts
index dbca9703f5ca..6596969de7f4 100644
--- a/src/content-render/unified/rewrite-local-links.ts
+++ b/src/content-render/unified/rewrite-local-links.ts
@@ -8,6 +8,7 @@ import { distance } from 'fastest-levenshtein'
import { getPathWithoutLanguage, getVersionStringFromPath } from '@/frame/lib/path-utils'
import { getNewVersionedPath } from '@/archives/lib/old-versions-utils'
import patterns from '@/frame/lib/patterns'
+import { createLogger } from '@/observability/logger'
import { deprecated, latest } from '@/versions/lib/enterprise-server-releases'
import nonEnterpriseDefaultVersion from '@/versions/lib/non-enterprise-default-version'
import { allVersions } from '@/versions/lib/all-versions'
@@ -16,6 +17,8 @@ import readJsonFile from '@/frame/lib/read-json-file'
import findPage from '@/frame/lib/find-page'
import type { Context, Page } from '@/types'
+const logger = createLogger(import.meta.url)
+
const isProd = process.env.NODE_ENV === 'production'
// This way, if you *set* the `LOG_ERROR_ANNOTATIONS` env var, whatever its
@@ -47,7 +50,8 @@ function logError(file: string, line: number, message: string, title = 'Error')
message.replace(/%/g, '%25').replace(/\r/g, '%0D').replace(/\n/g, '%0A'),
)
const error = `::error file=${file},line=${line},title=${title}::${message}`
- console.log(error)
+ process.stdout.write(`${error}\n`)
+ logger.info('GitHub Actions error annotation', { annotation: error })
}
}
@@ -109,7 +113,7 @@ export default function rewriteLocalLinks(context?: Context) {
mutableNode.url = definition.url
mutableNode.title = definition.title
} else {
- console.warn(`Definition not found for identifier: ${linkRefNode.identifier}`)
+ logger.warn('Definition not found for identifier', { identifier: linkRefNode.identifier })
}
})
@@ -246,13 +250,9 @@ function getNewHref(node: LinkNode, languageCode: string, version: string): stri
// This can happen if you have something
// like `/enterprise-servr@3.9/foo/bar` which is a typo. I.e.
// `enterprise-servr` is not a valid plan, but it has a `@` character in it.
- console.warn(
- `
-Warning! The first segment of the internal link has a '@' character in it
-but the plan is not recognized. This is likely a typo.
-Please inspect the link and fix it if it's a typo.
-Look for an internal link that starts with '${url}'.
- `,
+ logger.warn(
+ 'First segment of internal link has @ character but plan is not recognized, likely a typo',
+ { url },
)
}