From 85855abd7a66414f4de19a703cb723e243dd1bcf Mon Sep 17 00:00:00 2001 From: Jenni C <97056108+dihydroJenoxide@users.noreply.github.com> Date: Mon, 8 Jun 2026 10:51:01 -0700 Subject: [PATCH 1/3] new models not available for legacy annual billing (#61614) --- .../model-multipliers-for-annual-plans.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/content/copilot/reference/copilot-billing/request-based-billing-legacy/model-multipliers-for-annual-plans.md b/content/copilot/reference/copilot-billing/request-based-billing-legacy/model-multipliers-for-annual-plans.md index 4049722b57c5..e3860f59c925 100644 --- a/content/copilot/reference/copilot-billing/request-based-billing-legacy/model-multipliers-for-annual-plans.md +++ b/content/copilot/reference/copilot-billing/request-based-billing-legacy/model-multipliers-for-annual-plans.md @@ -24,7 +24,7 @@ The models included with {% data variables.product.prodname_copilot_short %} pla Model multipliers and costs are subject to change. -> [!NOTE] If you use {% data variables.copilot.copilot_auto_model_selection_short %} in {% data variables.copilot.copilot_chat_short %}, {% data variables.copilot.copilot_cli_short %}, or {% data variables.copilot.copilot_cloud_agent %}, you qualify for a 10% discount. For example, if a model has a multiplier of 1x you'll be billed at 0.9x instead. +> [!NOTE] Users on legacy annual {% data variables.product.prodname_copilot_short %} plans will not receive access to new models and features. ## Model multipliers @@ -36,6 +36,8 @@ The following table shows the model multipliers per supported model. > * {% data variables.copilot.copilot_claude_sonnet_46 %} > * {% data variables.copilot.copilot_gpt_54_mini %} > * The multiplier for {% data variables.copilot.copilot_mai_code_1_flash %} is a promotional rate. +> +> If you use {% data variables.copilot.copilot_auto_model_selection_short %} in {% data variables.copilot.copilot_chat_short %}, {% data variables.copilot.copilot_cli_short %}, or {% data variables.copilot.copilot_cloud_agent %}, you qualify for a 10% discount. For example, if a model has a multiplier of 1x you'll be billed at 0.9x instead. | Model | Multiplier | | --- | ---: | From b27140c8e60cecd75fcb3877101347d4c05a4754 Mon Sep 17 00:00:00 2001 From: Haripriya Chintalapati Date: Mon, 8 Jun 2026 13:51:20 -0400 Subject: [PATCH 2/3] Add supported versioning tags for Cargo, pip, Gradle, Elm, Docker, Go modules, and git submodule (and refine Bundler) (#61599) Co-authored-by: v-HaripriyaC Co-authored-by: mc <42146119+mchammer01@users.noreply.github.com> --- .../dependabot-updates-supported-versioning-tags.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/data/reusables/dependabot/dependabot-updates-supported-versioning-tags.md b/data/reusables/dependabot/dependabot-updates-supported-versioning-tags.md index 0a98642a0505..ab94cc72e234 100644 --- a/data/reusables/dependabot/dependabot-updates-supported-versioning-tags.md +++ b/data/reusables/dependabot/dependabot-updates-supported-versioning-tags.md @@ -19,6 +19,11 @@ The `dependabot.yml` file doesn't control the versioning tags that you can use, | pipenv | `pip` | `a`, `b`, `rc`, `dev`, `post` | `requests@1.0.0a1`, `numpy@2.0.0b3`, `django@4.0rc1`, `black@1.0.0.dev5`, `pandas@2.0.5.post1` | | pip-compile | `pip` | `a`, `b`, `rc`, `dev`, `post` | `requests@1.0.0a1`, `numpy@2.0.0b3`, `django@4.0rc1`, `black@1.0.0.dev5`, `pandas@2.0.5.post1` | | poetry | `pip` | `a`, `b`, `rc`, `dev`, `post` | `requests@1.0.0a1`, `numpy@2.0.0b3`, `django@4.0rc1`, `black@1.0.0.dev5`, `pandas@2.0.5.post1` | +| Gradle | `gradle` | `alpha`, `a`, `beta`, `b`, `milestone`, `m`, `rc`, `cr`, `snapshot`, `ga`, `final`, `release`, `sp` (case-insensitive) | `spring-boot-starter@3.0.0-RC1`, `kotlin-stdlib@2.0.0-beta`, `guava@33.0.0-SNAPSHOT`, `junit@5.10.0-M2`, `ktor@2.3.0-rc.1` | +| Elm | `elm` | None—strict `MAJOR.MINOR.PATCH` only (no pre-release versions) | `elm/core@1.0.0`, `elm/html@2.3.1`, `elm/json@10.0.0` | +| Docker | `docker` | `alpha`, `beta`, `rc`, `dev`, `preview`, `pre`, `nightly`, `snapshot`, `canary`, `unstable` (heuristic detection) | `nginx@1.25.0-rc1`, `node@20.0.0-alpha.1`, `redis@7.0.0-nightly`, `alpine@3.18.0-dev`, `ubuntu@22.04-preview` | +| git submodule | `gitsubmodule` | None—pins to commit SHAs or git tags (no versioning scheme) | `my-lib@abc1234`, `shared-utils@v1.2.0` | +| Go modules | `gomod` | `alpha`, `beta`, `rc` (SemVer prerelease after `-`) | `github.com/go-chi/chi@v5.0.0-rc1`, `google.golang.org/grpc@v1.60.0-beta.1`, `github.com/octo-org/octo-module@v0.17.0-alpha.1` | #### Ecosystem-specific versioning details @@ -26,4 +31,8 @@ The following details describe how {% data variables.product.prodname_dependabot * **Bundler:** Bundler does not use a fixed set of prerelease tags. Any version segment containing a letter is treated as a prerelease (for example, `.alpha`, `.beta1`, `.rc2`). Hyphens in version strings are normalized to `.pre.` internally (for example, `1.0.0-beta` becomes `1.0.0.pre.beta`). * **Cargo:** Follows SemVer 2.0.0 prerelease conventions. Anything after `-` is a prerelease identifier (dot-separated, `[0-9A-Za-z-]`). Build metadata (`+...`) is allowed but ignored for version precedence. +* **Gradle:** Aliases are also recognized: `pr`/`pre`/`preview`→`rc`, `eap`/`ea`→`alpha`. Additional prerelease qualifiers include `dev`, `experimental`, and `unstable`. Qualifiers are ordered by precedence: `alpha`/`a` < `beta`/`b` < `milestone`/`m` < `rc`/`cr` < `snapshot` < (empty/`ga`/`final`/`release`) < `sp`. Free-form identifiers not in this list are treated as stable. * **pip/pipenv/pip-compile/poetry (PEP 440):** The table lists canonical prerelease and postrelease suffixes per PEP 440. Aliases are also recognized and normalized to their canonical forms (`alpha`→`a`, `beta`→`b`, `c`/`pre`/`preview`→`rc`, `rev`/`r`→`post`). Epoch versions (`N!...`) and local versions (`+local`) are supported; local version segments are used only to break ties when the public version is identical. +* **Elm:** The Elm package registry enforces strict SemVer (`MAJOR.MINOR.PATCH` integers only) and does not allow publishing pre-release versions. Dependabot compares versions numerically. +* **Go modules:** Follows SemVer with a mandatory `v` prefix. Pseudo-versions (`v0.0.0-YYYYMMDDHHMMSS-commithash`) are used for unreleased commits and are always treated as pre-release. The `+incompatible` suffix marks modules at major version 2+ without a `go.mod` file and does not affect version ordering. +* **git submodule:** Dependabot tracks the latest commit on the configured branch. There is no version comparison—updates always move the pinned SHA forward. If the submodule tracks a tag, Dependabot follows the tag's commit. From 4a5568d0264171b5b8b57e8ebd320a522239ee5a Mon Sep 17 00:00:00 2001 From: Daniel Klemm Date: Mon, 8 Jun 2026 19:20:51 +0000 Subject: [PATCH 3/3] Exclude RAI content-filter 400s from AI search error monitor (#61498) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/search/lib/ai-search-constants.ts | 6 ++ src/search/lib/ai-search-proxy.ts | 32 +++++- src/search/tests/ai-search-proxy.ts | 148 ++++++++++++++++++++++++++ 3 files changed, 183 insertions(+), 3 deletions(-) create mode 100644 src/search/tests/ai-search-proxy.ts diff --git a/src/search/lib/ai-search-constants.ts b/src/search/lib/ai-search-constants.ts index c40b9e47b74e..15f0a6e297c0 100644 --- a/src/search/lib/ai-search-constants.ts +++ b/src/search/lib/ai-search-constants.ts @@ -2,3 +2,9 @@ // payloads are almost always pasted docs pages or unrelated content // and either time out or return no-answer. See github/cse-copilot#1214. export const MAX_QUERY_LENGTH = 500 + +// cse-copilot returns HTTP 400 with this code in `detail.code` when Azure's +// Responsible AI input content filter rejects a query. These are expected, +// user-triggered rejections, so they are tracked separately and kept out of +// the AI search error-rate monitor. See github/cse-copilot#1214. +export const RAI_CONTENT_FILTER_CODE = 'RAI_INPUT_CONTENT_POLICY_BREACH_ERROR' diff --git a/src/search/lib/ai-search-proxy.ts b/src/search/lib/ai-search-proxy.ts index 84f9833fe0c4..6ae2a44e98eb 100644 --- a/src/search/lib/ai-search-proxy.ts +++ b/src/search/lib/ai-search-proxy.ts @@ -6,7 +6,7 @@ import { getHmacWithEpoch } from '@/search/lib/helpers/get-cse-copilot-auth' import { getCSECopilotSource } from '@/search/lib/helpers/cse-copilot-docs-versions' import type { ExtendedRequest } from '@/types' import { handleExternalSearchAnalytics } from '@/search/lib/helpers/external-search-analytics' -import { MAX_QUERY_LENGTH } from '@/search/lib/ai-search-constants' +import { MAX_QUERY_LENGTH, RAI_CONTENT_FILTER_CODE } from '@/search/lib/ai-search-constants' const logger = createLogger(import.meta.url) @@ -15,6 +15,27 @@ const logger = createLogger(import.meta.url) // established, but the connect + first-byte must complete within this window. const AI_SEARCH_TIMEOUT_MS = 9_000 +type ContentFilterCandidate = { + status: number + headers: { get: (name: string) => string | null } + json: () => Promise +} + +const isContentFilterRejection = async (response: ContentFilterCandidate): Promise => { + if (response.status !== 400) { + return false + } + if (!response.headers.get('content-type')?.includes('application/json')) { + return false + } + try { + const body = (await response.json()) as { detail?: { code?: string } } + return body?.detail?.code === RAI_CONTENT_FILTER_CODE + } catch { + return false + } +} + export const aiSearchProxy = async (req: ExtendedRequest, res: Response) => { const { query, version } = req.body ?? {} @@ -100,8 +121,13 @@ export const aiSearchProxy = async (req: ExtendedRequest, res: Response) => { if (!response.ok) { const errorMessage = `Upstream server responded with status code ${response.status}` - logger.error(errorMessage, { statusCode: response.status }) - statsd.increment('ai-search.stream_response_error', 1, diagnosticTags) + if (await isContentFilterRejection(response)) { + logger.info(errorMessage, { statusCode: response.status }) + statsd.increment('ai-search.content_filtered', 1, diagnosticTags) + } else { + logger.error(errorMessage, { statusCode: response.status }) + statsd.increment('ai-search.stream_response_error', 1, diagnosticTags) + } res.status(response.status).json({ errors: [{ message: errorMessage }], upstreamStatus: response.status, diff --git a/src/search/tests/ai-search-proxy.ts b/src/search/tests/ai-search-proxy.ts new file mode 100644 index 000000000000..18342c17f5c6 --- /dev/null +++ b/src/search/tests/ai-search-proxy.ts @@ -0,0 +1,148 @@ +import { describe, test, expect, vi, beforeEach } from 'vitest' + +import statsd from '@/observability/lib/statsd' +import { fetchStream } from '@/frame/lib/fetch-utils' +import { aiSearchProxy } from '@/search/lib/ai-search-proxy' +import { RAI_CONTENT_FILTER_CODE } from '@/search/lib/ai-search-constants' +import type { ExtendedRequest } from '@/types' + +vi.mock('@/observability/lib/statsd', () => ({ + default: { increment: vi.fn(), gauge: vi.fn() }, +})) + +vi.mock('@/frame/lib/fetch-utils', () => ({ + fetchStream: vi.fn(), +})) + +vi.mock('@/search/lib/helpers/get-cse-copilot-auth', () => ({ + getHmacWithEpoch: () => 'test-auth', +})) + +vi.mock('@/search/lib/helpers/cse-copilot-docs-versions', () => ({ + getCSECopilotSource: () => 'docs', +})) + +vi.mock('@/search/lib/helpers/external-search-analytics', () => ({ + handleExternalSearchAnalytics: async () => null, +})) + +const incrementedMetrics = () => + (statsd.increment as ReturnType).mock.calls.map((call) => call[0]) + +function buildResponse() { + const res = { + statusCode: 0, + body: null as unknown, + status(code: number) { + this.statusCode = code + return this + }, + json(payload: unknown) { + this.body = payload + return this + }, + setHeader: vi.fn(), + flushHeaders: vi.fn(), + write: vi.fn(), + end: vi.fn(), + headersSent: false, + } + return res +} + +function buildRequest(): ExtendedRequest { + return { + body: { query: 'a disallowed query', version: 'dotcom' }, + language: 'en', + } as unknown as ExtendedRequest +} + +function mockUpstream( + status: number, + jsonBody: unknown | (() => never), + contentType = 'application/json', +) { + const json = vi.fn(async () => { + if (typeof jsonBody === 'function') { + return (jsonBody as () => never)() + } + return jsonBody + }) + ;(fetchStream as ReturnType).mockResolvedValue({ + ok: status < 400, + status, + headers: { + get: (name: string) => (name.toLowerCase() === 'content-type' ? contentType : null), + }, + json, + }) + return json +} + +describe('aiSearchProxy upstream 400 handling', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + test('RAI content filter 400 increments content_filtered, not stream_response_error', async () => { + mockUpstream(400, { + code: 400, + message: 'Responsible AI input content policy breach', + detail: { code: RAI_CONTENT_FILTER_CODE }, + }) + const res = buildResponse() + + await aiSearchProxy(buildRequest(), res as never) + + const metrics = incrementedMetrics() + expect(metrics).toContain('ai-search.content_filtered') + expect(metrics).not.toContain('ai-search.stream_response_error') + expect(metrics).toContain('ai-search.call') + expect(res.statusCode).toBe(400) + }) + + test('non-RAI 400 increments stream_response_error', async () => { + mockUpstream(400, { code: 400, message: 'some other bad request' }) + const res = buildResponse() + + await aiSearchProxy(buildRequest(), res as never) + + const metrics = incrementedMetrics() + expect(metrics).toContain('ai-search.stream_response_error') + expect(metrics).not.toContain('ai-search.content_filtered') + expect(res.statusCode).toBe(400) + }) + + test('malformed 400 body increments stream_response_error', async () => { + mockUpstream(400, () => { + throw new Error('invalid json') + }) + const res = buildResponse() + + await aiSearchProxy(buildRequest(), res as never) + + const metrics = incrementedMetrics() + expect(metrics).toContain('ai-search.stream_response_error') + expect(metrics).not.toContain('ai-search.content_filtered') + expect(res.statusCode).toBe(400) + }) + + test('non-JSON 400 body increments stream_response_error without parsing', async () => { + const json = mockUpstream( + 400, + () => { + throw new Error('should not be parsed') + }, + 'text/html', + ) + const res = buildResponse() + + await aiSearchProxy(buildRequest(), res as never) + + const metrics = incrementedMetrics() + expect(json).not.toHaveBeenCalled() + expect(metrics).toContain('ai-search.stream_response_error') + expect(metrics).not.toContain('ai-search.content_filtered') + expect(res.statusCode).toBe(400) + }) +})