-
-
Notifications
You must be signed in to change notification settings - Fork 286
Feat/enterprise and reasoning #154
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Conversation
- Add --enterprise-url flag to auth and start commands - Interactive auth prompts for enterprise host configuration - Persist enterprise URL in ~/.local/share/copilot-api/enterprise_url - Update OAuth flows to use enterprise endpoints (device code, access token) - Update Copilot API calls to use enterprise API endpoints - Add URL normalization helpers and validation - Add comprehensive unit tests for URL helpers - Update README.md and CLAUDE.md with enterprise usage examples - Backwards compatible: defaults to github.com when no enterprise configured 🤖 Generated with [Claude Code](https://claude.com/claude-code)
- Update copilotBaseUrl to use copilot-api.{enterprise} for GHE
- Add 6 new tests for copilotBaseUrl with enterprise configuration
- Update CLAUDE.md with correct enterprise endpoint documentation
- Fixes 'Failed to get models' error for GitHub Enterprise users
🤖 Generated with [Claude Code](https://claude.com/claude-code)
- Remove verbose documentation files - Simplify CLAUDE.md to concise format - Add CHANGELOG.md with version 0.8.0 release notes - Bump package.json version to 0.8.0
- Remove unused looksLikeHost() function and tests - Remove unnecessary type casts in url.ts - Centralize enterprise URL reading from state - Replace typeof checks with consistent function calls - Update GITHUB_BASE_URL and GITHUB_API_BASE_URL to read from state.enterpriseUrl - Remove enterpriseUrl parameters from service functions (getDeviceCode, pollAccessToken) - Update integration tests to set state.enterpriseUrl instead of passing parameters - Improve code consistency and maintainability All 64 tests passing. TypeScript compiles successfully. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
…g order when stream=false and exclude reasoning_opaque from token calculation in calculateMessageTokens
- Add signature field to AnthropicThinkingBlock - Add thinkingBlockOpen state tracking - Refactor stream translation with modular handlers - Support reasoning_opaque and reasoning_text fields - Exclude reasoning_opaque from token calculation - Set Bun server idleTimeout to 0 - Update tests for reasoning support Merged from caozhiyuan/copilot-api feature/chat-completions-reasoning branch
WalkthroughThis PR adds GitHub Enterprise Server (GHES) support to the copilot-api CLI, introducing enterprise URL configuration via CLI flags and interactive prompts, persistent enterprise URL storage, enterprise-aware OAuth and API endpoint routing, and extended message handling for Anthropic thinking blocks with reasoning fields. Changes
Sequence DiagramsequenceDiagram
participant User as User/CLI
participant Auth as auth.ts
participant Token as token.ts
participant State as state
participant GHSvc as GitHub Service
participant GHEnt as GitHub (default/enterprise)
rect rgb(200, 220, 255)
note over User,State: Enterprise URL Configuration Flow
User->>Auth: auth --enterprise-url [URL]
Auth->>State: set enterpriseUrl
Auth->>Token: setupGitHubToken({enterpriseUrl})
Token->>Token: readEnterpriseUrl() from persistent storage
Token->>State: state.enterpriseUrl = url
end
rect rgb(220, 240, 220)
note over Token,GHEnt: Device Code & Token Exchange (Enterprise-Aware)
Token->>GHSvc: getDeviceCode()
GHSvc->>GHSvc: baseUrl = GITHUB_BASE_URL() (uses state.enterpriseUrl)
alt enterprise configured
GHSvc->>GHEnt: POST https://ghe.example.com/login/device/code
else default github.com
GHSvc->>GHEnt: POST https://github.com/login/device/code
end
GHEnt-->>GHSvc: device_code, user_code
User->>User: Authorize device
Token->>GHSvc: pollAccessToken()
GHSvc->>GHEnt: POST [GITHUB_BASE_URL()]/login/oauth/access_token
GHEnt-->>GHSvc: access_token
Token->>Token: writeEnterpriseUrl(state.enterpriseUrl)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Areas requiring extra attention:
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning, 1 inconclusive)
✅ Passed checks (1 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
Tip 📝 Customizable high-level summaries are now available in beta!You can now customize how CodeRabbit generates the high-level summary in your pull requests — including its content, structure, tone, and formatting.
Example instruction:
Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 4
🧹 Nitpick comments (10)
CLAUDE.md (1)
38-41: Hard-coded line numbers will become stale.Referencing specific line numbers (e.g.,
src/start.ts:123) creates maintenance burden. Consider removing line numbers or using more durable references like function/symbol names.README.md (1)
167-167: Consider formatting the URL example.The bare URL in the table could benefit from backticks for better rendering and to satisfy markdown linting.
Apply this diff:
-| --enterprise-url | GitHub Enterprise host to use (eg. https://ghe.example.com) | none | none | +| --enterprise-url | GitHub Enterprise host to use (eg. `https://ghe.example.com`) | none | none |src/lib/state.ts (1)
6-6: State extension for enterpriseUrl is consistentAdding optional
enterpriseUrlwith a default ofundefinedis backward compatible and aligns with how the URL helpers consume it; you may optionally document whether this is expected to be a bare host vs. full URL to avoid future misuse.Also applies to: 26-26
tests/anthropic-request.test.ts (1)
137-157: Tests correctly cover reasoning_text; consider aligning Zod schemaThe added
signaturefields andreasoning_textassertions give good coverage for the new thinking-block translation behavior while preserving final text content and tool calls. Note thatmessageSchemaused byisValidChatCompletionRequestdoes not declarereasoning_text, so Zod won’t validate its presence or type; if you want this field treated as part of the validated payload shape, consider adding it (and any related fields likereasoning_opaque) tomessageSchema.Also applies to: 169-201
tests/url.test.ts (1)
1-66: URL helper tests are solid; please confirm GHES API base patternThe tests nicely exercise domain normalization and base URL helpers. For
githubApiBaseUrl, you’re assertinghttps://api.ghe.example.comwhen an enterprise host likeghe.example.comis provided; if this helper is intended specifically for GitHub Enterprise Server, you may want to double-check whether your deployment expects anapi.<host>subdomain versus the more common<host>/api/...pattern and adjustgithubApiBaseUrl(and these tests) accordingly if needed.tests/enterprise-persistence.test.ts (1)
1-153: Consider testing the actual persistence functions from the codebase.The tests directly use
fsmodule operations rather than testing the actualreadEnterpriseUrlandwriteEnterpriseUrlfunctions fromsrc/lib/token.ts. Additionally, the URL normalization tests (lines 119-152) duplicate the normalization logic instead of callingnormalizeDomainfromsrc/lib/url.ts.Consider refactoring to:
- Import and test
readEnterpriseUrlandwriteEnterpriseUrlfromsrc/lib/token.ts- Use
normalizeDomainfromsrc/lib/url.tsin normalization tests- Set up the actual PATHS configuration for the test environment
This would provide better integration testing and catch issues if the actual implementation changes. Example:
import { readEnterpriseUrl, writeEnterpriseUrl } from "../src/lib/token" import { normalizeDomain } from "../src/lib/url" // In setup, configure PATHS to use TEST_APP_DIR // Then test the actual functions: await writeEnterpriseUrl("ghe.example.com") const result = await readEnterpriseUrl() expect(result).toBe("ghe.example.com")CHANGELOG.md (1)
29-29: Optional: Consider capitalizing "GitHub.com" for consistency.The official name is typically written as "GitHub.com" rather than "github.com" in documentation.
-- 100% backwards compatible - defaults to github.com when no enterprise configured +- 100% backwards compatible - defaults to GitHub.com when no enterprise configuredsrc/lib/url.ts (1)
1-4: Consider validating URL format after normalization.The
normalizeDomainfunction could return an empty string if the input contains only a protocol (e.g.,"https://"). While the falsy check on line 2 protects against undefined/null, it doesn't catch this edge case.Consider adding validation:
export function normalizeDomain(url: string | undefined): string | undefined { if (!url) return undefined - return url.replace(/^https?:\/\//, "").replace(/\/+$/, "") + const normalized = url.replace(/^https?:\/\//, "").replace(/\/+$/, "") + return normalized || undefined }This ensures that invalid inputs like
"https://"or"///"returnundefinedrather than an empty string, maintaining consistent behavior with the falsy input case.test-enterprise.sh (1)
31-33: Avoid hardcoding the test count.Line 32 hardcodes "61 tests", which will become stale as tests are added or removed.
Consider removing the specific count or extracting it dynamically:
echo -e "${YELLOW}Test 5: Run all tests${NC}" -bun test --silent && echo -e "${GREEN}✓ All 61 tests pass${NC}" +bun test --silent && echo -e "${GREEN}✓ All tests pass${NC}" echo ""Or if you want to show the count dynamically:
TEST_OUTPUT=$(bun test --silent 2>&1) TEST_COUNT=$(echo "$TEST_OUTPUT" | grep -oP '\d+(?= tests)' || echo "unknown") echo -e "${GREEN}✓ All $TEST_COUNT tests pass${NC}"src/routes/messages/stream-translation.ts (1)
49-96: handleFinish correctly sequences block closure and message completion.The finish handler properly:
- Closes any open content block
- Emits reasoning opaque if the closed block wasn't a tool block
- Sends message_delta with stop_reason and usage
- Sends message_stop
The
context.events.pushon line 61 should useeventsdirectly for consistency with the rest of the function.if (state.contentBlockOpen) { const toolBlockOpen = isToolBlockOpen(state) - context.events.push({ + events.push({ type: "content_block_stop", index: state.contentBlockIndex, })
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (29)
CHANGELOG.md(1 hunks)CLAUDE.md(1 hunks)README.md(4 hunks)package.json(1 hunks)src/auth.ts(3 hunks)src/lib/api-config.ts(4 hunks)src/lib/paths.ts(1 hunks)src/lib/state.ts(2 hunks)src/lib/token.ts(4 hunks)src/lib/tokenizer.ts(1 hunks)src/lib/url.ts(1 hunks)src/routes/messages/anthropic-types.ts(2 hunks)src/routes/messages/handler.ts(1 hunks)src/routes/messages/non-stream-translation.ts(6 hunks)src/routes/messages/stream-translation.ts(3 hunks)src/services/copilot/create-chat-completions.ts(4 hunks)src/services/github/get-copilot-token.ts(1 hunks)src/services/github/get-copilot-usage.ts(1 hunks)src/services/github/get-device-code.ts(1 hunks)src/services/github/get-user.ts(1 hunks)src/services/github/poll-access-token.ts(2 hunks)src/start.ts(6 hunks)test-enterprise.sh(1 hunks)tests/anthropic-request.test.ts(4 hunks)tests/anthropic-response.test.ts(2 hunks)tests/copilot-base-url.test.ts(1 hunks)tests/enterprise-integration.test.ts(1 hunks)tests/enterprise-persistence.test.ts(1 hunks)tests/url.test.ts(1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-11-24T05:50:12.345Z
Learnt from: caozhiyuan
Repo: ericc-ch/copilot-api PR: 142
File: src/routes/messages/responses-stream-translation.ts:451-462
Timestamp: 2025-11-24T05:50:12.345Z
Learning: In src/routes/messages/responses-stream-translation.ts, the handleErrorEvent function intentionally does not call closeAllOpenBlocks before sending an error event to the client. This is acceptable in their error handling design - clients handle error events without requiring balanced content_block_stop events.
Applied to files:
src/routes/messages/handler.tssrc/routes/messages/stream-translation.ts
📚 Learning: 2025-11-11T04:33:30.522Z
Learnt from: caozhiyuan
Repo: ericc-ch/copilot-api PR: 142
File: src/routes/messages/handler.ts:50-52
Timestamp: 2025-11-11T04:33:30.522Z
Learning: In src/routes/messages/handler.ts, forcing anthropicPayload.model to getSmallModel() when no tools are present is intentional behavior to fix Claude Code 2.0.28 warmup requests consuming premium model tokens. This applies to all requests without tools, not just warmup requests, and is an accepted design decision.
Applied to files:
src/routes/messages/handler.tssrc/routes/messages/stream-translation.tstests/anthropic-request.test.tssrc/routes/messages/non-stream-translation.ts
🧬 Code graph analysis (14)
src/lib/token.ts (2)
src/lib/paths.ts (1)
PATHS(10-14)src/lib/state.ts (1)
state(21-27)
src/services/github/get-copilot-usage.ts (2)
src/lib/api-config.ts (2)
GITHUB_API_BASE_URL(49-49)githubHeaders(50-58)src/lib/state.ts (1)
state(21-27)
src/auth.ts (1)
src/lib/token.ts (1)
setupGitHubToken(63-113)
src/start.ts (2)
src/lib/state.ts (1)
state(21-27)src/lib/token.ts (1)
setupGitHubToken(63-113)
tests/copilot-base-url.test.ts (2)
src/lib/state.ts (2)
state(21-27)State(3-19)src/lib/api-config.ts (1)
copilotBaseUrl(19-29)
src/routes/messages/stream-translation.ts (3)
src/services/copilot/create-chat-completions.ts (3)
Choice(88-93)ChatCompletionChunk(51-70)Delta(72-86)src/routes/messages/anthropic-types.ts (2)
AnthropicStreamState(196-208)AnthropicStreamEventData(185-193)src/routes/messages/utils.ts (1)
mapOpenAIStopReasonToAnthropic(3-16)
src/services/github/poll-access-token.ts (1)
src/lib/api-config.ts (1)
GITHUB_BASE_URL(60-60)
tests/url.test.ts (1)
src/lib/url.ts (3)
normalizeDomain(1-4)githubBaseUrl(6-10)githubApiBaseUrl(12-16)
src/services/github/get-user.ts (1)
src/lib/api-config.ts (1)
GITHUB_API_BASE_URL(49-49)
src/services/github/get-copilot-token.ts (1)
src/lib/api-config.ts (1)
GITHUB_API_BASE_URL(49-49)
src/lib/api-config.ts (2)
src/lib/state.ts (2)
state(21-27)State(3-19)src/lib/url.ts (2)
githubApiBaseUrl(12-16)githubBaseUrl(6-10)
src/services/github/get-device-code.ts (1)
src/lib/api-config.ts (1)
GITHUB_BASE_URL(60-60)
src/routes/messages/non-stream-translation.ts (1)
src/routes/messages/anthropic-types.ts (3)
AnthropicTextBlock(28-31)AnthropicAssistantContentBlock(67-70)AnthropicThinkingBlock(56-60)
test-enterprise.sh (2)
src/auth.ts (1)
auth(53-83)src/start.ts (1)
start(128-218)
🪛 LanguageTool
CHANGELOG.md
[uncategorized] ~29-~29: The official name of this software platform is spelled with a capital “H”.
Context: ...100% backwards compatible - defaults to github.com when no enterprise configured ## [...
(GITHUB)
🪛 markdownlint-cli2 (0.18.1)
README.md
167-167: Bare URL used
(MD034, no-bare-urls)
175-175: Bare URL used
(MD034, no-bare-urls)
368-368: Bare URL used
(MD034, no-bare-urls)
🔇 Additional comments (53)
package.json (1)
3-3: LGTM!The version bump to 0.8.0 is appropriate for the new enterprise support features being introduced.
README.md (2)
42-42: LGTM!Enterprise support is now clearly documented in the features list.
262-269: LGTM!The enterprise usage examples are clear and cover both interactive and scripted authentication workflows.
src/services/copilot/create-chat-completions.ts (4)
72-86: LGTM!Exporting the
Deltainterface and adding optional reasoning fields (reasoning_text,reasoning_opaque) provides good support for Anthropic thinking blocks while maintaining backward compatibility.
88-93: LGTM!Exporting the
Choiceinterface improves type reusability across the codebase.
114-120: LGTM!Adding optional reasoning fields to
ResponseMessagemaintains consistency with the streaming types.
166-175: LGTM!Adding optional reasoning fields to the
Messageinterface allows reasoning content to be passed through the request/response chain.src/routes/messages/anthropic-types.ts (2)
196-208: LGTM!Adding
thinkingBlockOpento the stream state is necessary for tracking whether a thinking block is currently being streamed.
56-60: <!-- [scratchpad_start]
[task verify_thinking_block_signature]
[observations]
- Web search confirms: Anthropic thinking blocks always include signature when extended thinking is enabled
- Interface definition (line 56-60): signature is required (non-optional)
- Code pattern at line 151-153: defensive check for signature existence (
b.signature && b.signature.length > 0)- Construction logic (lines 343-354): creates thinking blocks with
signature: reasoningOpaque || ""(empty string fallback)- Line 349: thinking block created with empty thinking but populated signature
- Pattern shows code creates thinking blocks with potentially empty signature values
[analysis]
- Anthropic API: signature is always present in thinking blocks (never absent)
- Interface correctly reflects this (signature is required)
- Code defensive checks are actually checking for empty strings, not absence
- Code construction: intentionally creates thinking blocks with empty signature via fallback (
|| "")- This is contradictory to the interface contract
[conclusion]
The interface definition is CORRECT per Anthropic API spec. The signature field should NOT be optional.
The actual code issue is that construction logic at lines 343-354 creates thinking blocks with empty signatures, which violates the interface contract and Anthropic's API expectations. But that's not what the review comment is asking about.The review comment asks: "should signature be optional?" Answer: NO. It should remain required.
The interface is correct. The code constructing empty signatures is the real issue, but outside scope of this review.
[done]
</scratchpad_end] -->Signature field should remain required; the interface definition is correct.
Anthropic's thinking blocks always include a signature field when extended thinking is enabled. The interface correctly defines
signature: stringas required, not optional.The codebase does contain defensive checks (lines 151-153 in
non-stream-translation.tscheck if signature exists and has length), but this appears to be overly defensive logic rather than a legitimate case where thinking blocks lack signatures. Note that the construction logic (lines 343-354) creates thinking blocks with fallback empty strings (reasoningOpaque || ""), which contradicts the API contract but is a separate code quality concern, not a reason to make the field optional.src/services/github/get-copilot-usage.ts (1)
6-11: LGTM!Converting
GITHUB_API_BASE_URLfrom a constant to a function call correctly implements enterprise URL support, allowing the base URL to be computed dynamically based onstate.enterpriseUrl.src/services/github/get-user.ts (1)
6-6: LGTM!Converting
GITHUB_API_BASE_URLto a function call is consistent with the enterprise URL support pattern applied throughout the codebase.CLAUDE.md (1)
34-34: Endpoint patterns verified and accurate.All three documented enterprise endpoint patterns match the actual implementation:
- OAuth (
https://{enterprise}/...): Implemented bygithubBaseUrl(enterprise)insrc/lib/url.ts- GitHub API (
https://api.{enterprise}/...): Implemented bygithubApiBaseUrl(enterprise)insrc/lib/url.ts- Copilot (
https://copilot-api.{enterprise}/...): Implemented bycopilotBaseUrl(state)insrc/lib/api-config.tsTests confirm all patterns function as documented.
src/services/github/get-copilot-token.ts (1)
7-7: Enterprise-aware API base usage looks correctSwitching to
GITHUB_API_BASE_URL()keeps existing dotcom behavior while allowing enterprise routing viastate.enterpriseUrl; no further changes needed here.src/routes/messages/handler.ts (1)
58-64: Initialize thinkingBlockOpen in streaming stateIncluding
thinkingBlockOpen: falsein the initialAnthropicStreamStatekeeps the streaming translator in sync with the new thinking-block handling; this wiring looks correct.src/services/github/poll-access-token.ts (1)
6-6: Use GITHUB_BASE_URL() for enterprise-aware OAuth pollingUsing
GITHUB_BASE_URL()here is consistent with the new API config and ensures the/login/oauth/access_tokenendpoint targets the correct host for both dotcom and enterprise.Also applies to: 22-22
src/lib/paths.ts (1)
8-8: Enterprise URL path and creation mirror token handling appropriatelyDefining
ENTERPRISE_URL_PATHand ensuring it is created with0o600permissions keeps enterprise URL persistence consistent and secure alongside the GitHub token file.Also applies to: 13-13, 19-19
src/services/github/get-device-code.ts (1)
5-5: Device-code endpoint now respects enterprise base URLCalling
GITHUB_BASE_URL()when constructing the/login/device/codeURL aligns this flow with the new base-URL configuration for both dotcom and enterprise instances.Also applies to: 10-10
src/start.ts (2)
122-124: Verify the purpose of settingidleTimeout: 0.This change disables Bun's automatic idle connection cleanup, which is not mentioned in the PR objectives or changelog. Please confirm whether this is intentional and related to enterprise support, or if it was included by mistake.
If this is intentional, consider documenting the rationale in a code comment or the changelog.
28-28: LGTM! Enterprise URL handling is consistent.The enterprise URL parameter is properly threaded through the options interface, state management, CLI arguments, and token setup flow. The implementation follows the same pattern as
src/auth.ts.Also applies to: 50-50, 59-59, 192-196, 215-215
tests/anthropic-response.test.ts (1)
255-255: LGTM! Test state initialization updated correctly.The
thinkingBlockOpen: falsefield has been added to both test state initializations, consistent with the updatedAnthropicStreamStatetype.Also applies to: 356-356
tests/copilot-base-url.test.ts (1)
1-85: LGTM! Comprehensive test coverage for enterprise URL routing.The test suite provides excellent coverage of
copilotBaseUrlbehavior across different account types and enterprise URL configurations, including edge cases like subdomains and URL precedence.src/lib/url.ts (1)
6-16: LGTM! Enterprise URL utilities are well-designed.The functions provide clean abstractions for enterprise-aware GitHub endpoint resolution. The default fallback to
github.comensures backward compatibility.test-enterprise.sh (1)
1-76: Excellent test documentation and coverage.The script provides comprehensive coverage of both automated tests and clear instructions for manual verification scenarios. The structured approach to testing enterprise support is well-organized.
src/auth.ts (2)
24-49: Excellent UX with interactive enterprise configuration.The interactive prompt gracefully handles cases where users don't provide the
--enterprise-urlflag, with clear instructions on the expected format. The conditional spread operator cleanly handles the optional parameter.
13-13: LGTM! Enterprise URL parameter handling is consistent.The enterprise URL option is properly integrated into the auth command, matching the implementation pattern in
src/start.ts.Also applies to: 70-74, 80-80
src/lib/token.ts (5)
18-26: LGTM!The
readEnterpriseUrlhelper properly handles missing files by catching errors and returningundefined. Trimming the content and returningundefinedfor empty strings is a good defensive pattern.
28-29: Consider awaiting or handling the write promise.
writeEnterpriseUrlreturns a promise but callers may not be aware it's fire-and-forget. However, looking at line 97, it is properly awaited. The function itself is fine.
68-69: Persisted enterprise URL is loaded on every token setup.This correctly loads and applies the persisted enterprise URL to state when the token file exists. The flow ensures enterprise configuration persists across sessions.
82-86: Options-provided enterpriseUrl correctly overrides persisted value.The logic properly allows CLI-provided
enterpriseUrlto override any persisted value. The comment clarifies the intent.
96-97: Enterprise URL persisted alongside token.This ensures the enterprise URL is saved when a new token is acquired, maintaining consistency across restarts.
tests/enterprise-integration.test.ts (6)
14-19: Good test setup with proper state initialization.The
beforeEachcorrectly resetsfetchCalls, clearsenterpriseUrl, and sets up mock tokens. This ensures test isolation.
21-23: Proper cleanup restores original fetch.Restoring
globalThis.fetchinafterEachprevents test pollution.
25-99: Comprehensive getDeviceCode tests.Tests cover the three key scenarios: default github.com, enterprise URL, and URL normalization with https prefix. The assertions correctly verify the constructed URLs.
101-155: pollAccessToken tests validate OAuth endpoint routing.Good coverage of standard and enterprise OAuth token endpoints.
157-259: API endpoint tests (getCopilotToken, getCopilotUsage) are well structured.These tests verify the API URL resolution for both github.com and enterprise hosts. The
api.prefix handling for enterprise URLs is correctly tested.
261-301: getGitHubUser tests complete the integration coverage.Good validation of the user endpoint routing for both standard and enterprise configurations.
src/routes/messages/stream-translation.ts (8)
23-47: Clean orchestration of handlers.The main
translateChunkToAnthropicEventsfunction now clearly shows the processing flow: message start → thinking → content → tool calls → finish. This modular approach improves maintainability.
98-158: handleToolCalls manages tool block transitions correctly.The handler properly:
- Closes thinking blocks before tool calls
- Handles reasoning opaque when transitioning from non-tool content
- Closes previous blocks before starting new tool use blocks
- Tracks tool calls in state with their anthropic block indices
160-174: handleReasoningOpaqueInToolCalls handles the transition from content to tool calls.This helper ensures non-tool content blocks are closed and reasoning opaque is emitted before processing tool calls.
176-215: handleContent manages text block lifecycle.Properly closes thinking blocks and tool blocks before starting text content. The guard
!state.contentBlockOpenprevents duplicate block starts.
217-248: handleMessageStart initializes the stream correctly.Emits
message_startevent with proper structure including model, usage, and initial state. Theoutput_tokens: 0with comment about later update is clear.
250-288: handleReasoningOpaque emits complete thinking blocks.When
reasoning_opaqueexists without an open thinking block, this creates a complete thinking block sequence (start → delta → signature → stop) in one batch. This is useful for tool call responses with signatures.
290-336: handleThinkingText manages streaming thinking content.The handler:
- Opens thinking block on first thinking text
- Streams thinking_delta events
- Closes block with signature when reasoning_opaque arrives
The dual responsibility (opening block AND checking for signature closure) within the same handler is reasonable given the streaming nature.
338-360: closeThinkingBlockIfOpen provides clean block closure.Emits empty signature_delta before content_block_stop when closing an open thinking block. This ensures protocol compliance even when no signature was received.
src/lib/api-config.ts (3)
60-60: GITHUB_BASE_URL also converted to function.Same pattern as
GITHUB_API_BASE_URL- now dynamically resolves enterprise URLs.
50-58: githubHeaders expanded with additional fields.The headers now include editor-version, plugin-version, user-agent, and API version. This aligns with copilotHeaders and improves request consistency.
49-49: All call sites properly updated.Verification confirms all three call sites in the codebase (
get-user.ts,get-copilot-usage.ts, andget-copilot-token.ts) are correctly invokingGITHUB_API_BASE_URL()with function call syntax. The change successfully enables dynamic URL resolution based on enterprise configuration.src/routes/messages/non-stream-translation.ts (6)
146-153: Thinking content extraction is well structured.The logic correctly:
- Filters to non-empty thinking blocks
- Joins multiple thinking sections with double newlines
- Extracts the first non-empty signature found
This handles multi-block scenarios gracefully.
155-179: reasoning_text and reasoning_opaque added to both code paths.Both branches (with and without tool_calls) now include the reasoning fields in the assistant message. This ensures thinking content is preserved regardless of message type.
196-199: mapContent correctly excludes thinking blocks from text output.When there's no image, the function now explicitly filters to
texttype blocks, preventing thinking content from being duplicated in the text output (since it's represented separately via reasoning fields).
278-290: Thinking blocks reconstructed in translateToAnthropic response.The response construction now includes thinking blocks alongside text and tool use blocks. The order (thinking → text → tool_use) aligns with typical assistant response structure.
323-324: Good defensive check for empty string content.Adding
messageContent.length > 0prevents creating empty text blocks.
336-359: getAnthropicThinkBlocks handles all reasoning combinations.The helper correctly handles:
reasoning_textpresent → create thinking block with text and optional signature- Only
reasoning_opaque→ create thinking block with empty text but signature- Neither → return empty array
This covers the edge cases for signature-only responses.
| The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), | ||
| and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). | ||
|
|
||
| ## [0.8.0] - 2025-01-31 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix the release date.
The changelog shows release date as 2025-01-31, but this PR was created on 2025-11-25. Please update to the correct release date or use Unreleased if the version hasn't been released yet.
Apply this diff to use standard unreleased format:
-## [0.8.0] - 2025-01-31
+## [Unreleased]📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| ## [0.8.0] - 2025-01-31 | |
| ## [Unreleased] |
🤖 Prompt for AI Agents
In CHANGELOG.md around line 8, the release date for version 0.8.0 is incorrect
(shows 2025-01-31) — update that line to either the correct release date
(2025-11-25) or change the section header to use the standard "Unreleased"
format (e.g., "## [0.8.0] - Unreleased") depending on whether the release is
published; ensure the header matches the project's changelog style.
| - For GitHub Enterprise Server/Cloud users: Use `--enterprise-url` to specify your enterprise host (e.g., `--enterprise-url https://ghe.example.com`). The interactive auth command (`copilot-api auth`) will prompt you for your enterprise host if you don't provide it via the CLI flag. | ||
|
|
||
|
|
||
|
|
||
| export ANTHROPIC_BASE_URL=http://localhost:4141 ANTHROPIC_AUTH_TOKEN=dummy ANTHROPIC_MODEL=claude-sonnet-4.5 ANTHROPIC_DEFAULT_SONNET_MODEL=claude-sonnet-4.5 ANTHROPIC_SMALL_FAST_MODEL=gpt-5-mini ANTHROPIC_DEFAULT_HAIKU_MODEL=gpt-5-mini DISABLE_NON_ESSENTIAL_MODEL_CALLS=1 CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 && claude |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix malformed export command example.
Lines 365-368 appear to be improperly formatted. The export command is split across lines without proper markdown code block formatting, making it difficult to read and copy.
Apply this diff to properly format the export command:
- For GitHub Enterprise Server/Cloud users: Use `--enterprise-url` to specify your enterprise host (e.g., `--enterprise-url https://ghe.example.com`). The interactive auth command (`copilot-api auth`) will prompt you for your enterprise host if you don't provide it via the CLI flag.
-
-
-
-export ANTHROPIC_BASE_URL=http://localhost:4141 ANTHROPIC_AUTH_TOKEN=dummy ANTHROPIC_MODEL=claude-sonnet-4.5 ANTHROPIC_DEFAULT_SONNET_MODEL=claude-sonnet-4.5 ANTHROPIC_SMALL_FAST_MODEL=gpt-5-mini ANTHROPIC_DEFAULT_HAIKU_MODEL=gpt-5-mini DISABLE_NON_ESSENTIAL_MODEL_CALLS=1 CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 && claude
+
+Example command to launch Claude with Copilot API:
+
+```sh
+export ANTHROPIC_BASE_URL=http://localhost:4141 \
+ ANTHROPIC_AUTH_TOKEN=dummy \
+ ANTHROPIC_MODEL=claude-sonnet-4.5 \
+ ANTHROPIC_DEFAULT_SONNET_MODEL=claude-sonnet-4.5 \
+ ANTHROPIC_SMALL_FAST_MODEL=gpt-5-mini \
+ ANTHROPIC_DEFAULT_HAIKU_MODEL=gpt-5-mini \
+ DISABLE_NON_ESSENTIAL_MODEL_CALLS=1 \
+ CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1 && claude
+```🧰 Tools
🪛 markdownlint-cli2 (0.18.1)
368-368: Bare URL used
(MD034, no-bare-urls)
🤖 Prompt for AI Agents
In README.md around lines 364 to 368 the example export command is malformed and
split across lines without a fenced code block; wrap the environment-variable
block in a proper fenced code block (```sh), place the export on one logical
command using backslashes for line continuations so each ANTHROPIC_* and flag is
on its own indented line, and add the closing ``` to restore correct formatting
and copy-pastability.
| export const copilotBaseUrl = (state: State) => { | ||
| // If enterprise URL is configured, use enterprise Copilot API endpoint | ||
| if (state.enterpriseUrl) { | ||
| return `https://copilot-api.${state.enterpriseUrl}` | ||
| } | ||
|
|
||
| // Otherwise use standard GitHub Copilot endpoints based on account type | ||
| return state.accountType === "individual" ? | ||
| "https://api.githubcopilot.com" | ||
| : `https://api.${state.accountType}.githubcopilot.com` | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Enterprise URL not normalized in copilotBaseUrl.
The copilotBaseUrl function directly interpolates state.enterpriseUrl without normalization. If enterpriseUrl contains a protocol prefix (e.g., https://ghe.example.com), the resulting URL will be malformed: https://copilot-api.https://ghe.example.com.
Other functions use githubBaseUrl and githubApiBaseUrl from ./url.ts which call normalizeDomain. Consider using the same normalization here.
+import { normalizeDomain, githubBaseUrl, githubApiBaseUrl } from "./url"
+
export const copilotBaseUrl = (state: State) => {
// If enterprise URL is configured, use enterprise Copilot API endpoint
if (state.enterpriseUrl) {
- return `https://copilot-api.${state.enterpriseUrl}`
+ const domain = normalizeDomain(state.enterpriseUrl)
+ return `https://copilot-api.${domain}`
}
// Otherwise use standard GitHub Copilot endpoints based on account type
return state.accountType === "individual" ?
"https://api.githubcopilot.com"
: `https://api.${state.accountType}.githubcopilot.com`
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| export const copilotBaseUrl = (state: State) => { | |
| // If enterprise URL is configured, use enterprise Copilot API endpoint | |
| if (state.enterpriseUrl) { | |
| return `https://copilot-api.${state.enterpriseUrl}` | |
| } | |
| // Otherwise use standard GitHub Copilot endpoints based on account type | |
| return state.accountType === "individual" ? | |
| "https://api.githubcopilot.com" | |
| : `https://api.${state.accountType}.githubcopilot.com` | |
| } | |
| import { normalizeDomain, githubBaseUrl, githubApiBaseUrl } from "./url" | |
| export const copilotBaseUrl = (state: State) => { | |
| // If enterprise URL is configured, use enterprise Copilot API endpoint | |
| if (state.enterpriseUrl) { | |
| const domain = normalizeDomain(state.enterpriseUrl) | |
| return `https://copilot-api.${domain}` | |
| } | |
| // Otherwise use standard GitHub Copilot endpoints based on account type | |
| return state.accountType === "individual" ? | |
| "https://api.githubcopilot.com" | |
| : `https://api.${state.accountType}.githubcopilot.com` | |
| } |
🤖 Prompt for AI Agents
In src/lib/api-config.ts around lines 19 to 29, the function interpolates
state.enterpriseUrl directly which can include a protocol and produce malformed
URLs; update the function to import and call normalizeDomain (from ./url.ts) on
state.enterpriseUrl (or otherwise strip any protocol/trailing slashes) and use
the normalized domain when building the copilot API URL (e.g., const domain =
normalizeDomain(state.enterpriseUrl); return `https://copilot-api.${domain}`) so
the returned URL is always well-formed.
| if (key === "reasoning_opaque") { | ||
| continue | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
Verify token counting for reasoning fields.
The code excludes reasoning_opaque from token calculation, but reasoning_text (also added in this PR) is still counted. Confirm this asymmetry is intentional—typically, reasoning/thinking content should be excluded from input token counts since it's model-generated metadata.
Run the following script to check how both reasoning fields are used across the codebase:
🏁 Script executed:
#!/bin/bash
# Search for reasoning_text and reasoning_opaque usage patterns
echo "=== Searching for reasoning_text usage ==="
rg -nC3 'reasoning_text' --type=ts
echo -e "\n=== Searching for reasoning_opaque usage ==="
rg -nC3 'reasoning_opaque' --type=tsLength of output: 11191
I need to examine the full tokenizer logic to confirm whether reasoning_text is actually being counted and verify if this asymmetry is intentional.
Now let me check the full tokenizer implementation to confirm whether reasoning_text is being counted:
Let me check the complete tokenizer implementation to see if reasoning_text is actually being counted:
Exclude reasoning_text from token counting like reasoning_opaque.
The Anthropic API automatically ignores thinking blocks from previous turns and they are not included when calculating context usage. Both reasoning_text and reasoning_opaque in this codebase represent model-generated metadata from Anthropic responses (the thinking and encrypted signature fields respectively). When these are passed back in messages for multi-turn conversations, they should both be excluded from input token counting. Currently, only reasoning_opaque is skipped at lines 76-78; reasoning_text proceeds through normal token counting, creating an asymmetry that should be resolved.
Add a check to also skip reasoning_text:
if (key === "reasoning_opaque" || key === "reasoning_text") {
continue
}
🤖 Prompt for AI Agents
In src/lib/tokenizer.ts around lines 76 to 78, the tokenizer currently skips
only "reasoning_opaque" but still counts "reasoning_text", creating an
unintended asymmetry; update the conditional to also skip "reasoning_text" so
both model-generated thinking fields are excluded from input token counting
(i.e., treat "reasoning_text" the same as "reasoning_opaque" and continue past
it).
Summary by CodeRabbit
Release Notes - Version 0.8.0
New Features
--enterprise-urlflag for auth and start commandsChanges
Documentation
Tests
✏️ Tip: You can customize this high-level summary in your review settings.