feat: native Anthropic API mode for Claude models on GitHub Copilot (#579)

* feat: native Anthropic API mode for Claude models on GitHub Copilot

When using Claude models through GitHub Copilot, automatically switch from
the OpenAI-compatible shim to Anthropic's native messages API format.

The Copilot proxy (api.githubcopilot.com) supports Anthropic's native API
for Claude models. This enables cache_control blocks to be sent and
honoured, allowing explicit prompt caching control (as opposed to relying
solely on server-side auto-caching).

Changes:
- Add isGithubNativeAnthropicMode() in providers.ts that auto-enables when
  the resolved model starts with "claude-" and the GitHub provider is active
- Create a native Anthropic client in client.ts using the GitHub base URL
  and Bearer token authentication when native mode is detected
- Enable prompt caching in claude.ts for native GitHub mode so cache_control
  blocks are sent (previously only allowed for firstParty/bedrock/vertex)
- CLAUDE_CODE_GITHUB_ANTHROPIC_API=1 env var to force native mode for any
  model

Benefits:
- Proper Anthropic message format (no lossy OpenAI translation)
- Explicit cache_control blocks for fine-grained caching control
- Potentially better Claude model behaviour with native format

Related: #515

* fix: scope force flag to Claude models and add isGithubNativeAnthropicMode tests

- CLAUDE_CODE_GITHUB_ANTHROPIC_API=1 now returns false for non-Claude models
  (force flag still useful for aliases like 'github:copilot' with no model
  resolved yet, where it returns true when model is empty)
- Add 7 focused tests covering mode detection: off without GitHub provider,
  auto-detect via OPENAI_MODEL and resolvedModel, non-Claude model rejection,
  and force-flag behaviour for claude/non-claude/no-model cases

* fix: detect github:copilot:claude- compound format, remove force flag

OPENAI_MODEL for GitHub Copilot uses the format 'github:copilot:MODEL'
(e.g. 'github:copilot:claude-sonnet-4'), which does not start with 'claude-'.
Auto-detection now handles both bare model names and the compound format.

The CLAUDE_CODE_GITHUB_ANTHROPIC_API force flag is removed: with proper
compound-format detection there is no remaining gap it could fill, and
keeping a broad override flag without a concrete use case invites misuse.

Tests updated to cover the compound format, generic alias (false), and
non-Claude compound model (github:copilot:gpt-4o → false).

* fix: use includes('claude-') for model detection, remove force flag

Detection was broken for the standard GitHub Copilot compound format
'github:copilot:claude-sonnet-4' which does not start with 'claude-'.
Using includes('claude-') handles bare names, compound names, and any
future variants without needing updates.

The CLAUDE_CODE_GITHUB_ANTHROPIC_API force flag is removed as it was
a workaround for the broken detection, not a genuine use case.

---------

Co-authored-by: Zartris <14197299+Zartris@users.noreply.github.com>
This commit is contained in:
Zartris
2026-04-20 10:34:58 +02:00
committed by GitHub
parent 4cb963e660
commit fdef4a1b4c
4 changed files with 102 additions and 1 deletions

View File

@@ -107,3 +107,60 @@ test('official OpenAI base URLs now keep provider detection on openai for aliase
const { getAPIProvider } = await importFreshProvidersModule()
expect(getAPIProvider()).toBe('openai')
})
// isGithubNativeAnthropicMode
test('isGithubNativeAnthropicMode: false when CLAUDE_CODE_USE_GITHUB is not set', async () => {
clearProviderEnv()
process.env.OPENAI_MODEL = 'claude-sonnet-4-5'
const { isGithubNativeAnthropicMode } = await importFreshProvidersModule()
expect(isGithubNativeAnthropicMode()).toBe(false)
})
test('isGithubNativeAnthropicMode: true for bare claude- model via OPENAI_MODEL', async () => {
clearProviderEnv()
process.env.CLAUDE_CODE_USE_GITHUB = '1'
process.env.OPENAI_MODEL = 'claude-sonnet-4-5'
const { isGithubNativeAnthropicMode } = await importFreshProvidersModule()
expect(isGithubNativeAnthropicMode()).toBe(true)
})
test('isGithubNativeAnthropicMode: true for github:copilot:claude- compound format', async () => {
clearProviderEnv()
process.env.CLAUDE_CODE_USE_GITHUB = '1'
process.env.OPENAI_MODEL = 'github:copilot:claude-sonnet-4'
const { isGithubNativeAnthropicMode } = await importFreshProvidersModule()
expect(isGithubNativeAnthropicMode()).toBe(true)
})
test('isGithubNativeAnthropicMode: true when resolvedModel is a claude- model', async () => {
clearProviderEnv()
process.env.CLAUDE_CODE_USE_GITHUB = '1'
process.env.OPENAI_MODEL = 'github:copilot'
const { isGithubNativeAnthropicMode } = await importFreshProvidersModule()
expect(isGithubNativeAnthropicMode('claude-haiku-4-5')).toBe(true)
})
test('isGithubNativeAnthropicMode: false for generic github:copilot alias', async () => {
clearProviderEnv()
process.env.CLAUDE_CODE_USE_GITHUB = '1'
process.env.OPENAI_MODEL = 'github:copilot'
const { isGithubNativeAnthropicMode } = await importFreshProvidersModule()
expect(isGithubNativeAnthropicMode()).toBe(false)
})
test('isGithubNativeAnthropicMode: false for non-Claude model', async () => {
clearProviderEnv()
process.env.CLAUDE_CODE_USE_GITHUB = '1'
process.env.OPENAI_MODEL = 'gpt-4o'
const { isGithubNativeAnthropicMode } = await importFreshProvidersModule()
expect(isGithubNativeAnthropicMode()).toBe(false)
})
test('isGithubNativeAnthropicMode: false for github:copilot:gpt- model', async () => {
clearProviderEnv()
process.env.CLAUDE_CODE_USE_GITHUB = '1'
process.env.OPENAI_MODEL = 'github:copilot:gpt-4o'
const { isGithubNativeAnthropicMode } = await importFreshProvidersModule()
expect(isGithubNativeAnthropicMode()).toBe(false)
})