Skip to content

Conversation

@jkorsvik
Copy link

@jkorsvik jkorsvik commented Nov 25, 2025

Summary by CodeRabbit

Release Notes - Version 0.8.0

  • New Features

    • Added GitHub Enterprise Server/Cloud support with --enterprise-url flag for auth and start commands
    • Interactive enterprise configuration prompts and persistent URL storage
    • Automatic API endpoint routing for enterprise hosts
  • Changes

    • Enhanced message responses with reasoning fields
    • Improved URL normalization for enterprise domain handling
    • Version bumped to 0.8.0 with full backward compatibility
  • Documentation

    • Added comprehensive CHANGELOG and project documentation
    • Updated README with enterprise usage examples
    • Added environment variable configuration for Claude/Anthropic integration
  • Tests

    • Added 41+ tests covering enterprise functionality, URL handling, and API routing

✏️ Tip: You can customize this high-level summary in your review settings.

jkorsvik and others added 12 commits October 13, 2025 16:50
- 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
@coderabbitai
Copy link

coderabbitai bot commented Nov 25, 2025

Walkthrough

This 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

Cohort / File(s) Summary
Documentation
CHANGELOG.md, CLAUDE.md, README.md, package.json
Introduces CHANGELOG with v0.8.0 release notes detailing enterprise support features and changes. Adds CLAUDE.md project guidance. Updates README with enterprise CLI options and usage examples. Bumps version to 0.8.0.
Core Enterprise Authentication & Configuration
src/auth.ts, src/start.ts
Adds optional enterpriseUrl to RunAuthOptions and RunServerOptions. Introduces interactive enterprise URL prompts in auth flow. Wires --enterprise-url CLI option to auth and start commands with conditional parameter passing.
Enterprise State & Storage
src/lib/state.ts, src/lib/paths.ts, src/lib/token.ts
Adds enterpriseUrl?: string to State interface. Introduces ENTERPRISE_URL_PATH for persistent storage. Implements readEnterpriseUrl() and writeEnterpriseUrl() helpers. Updates SetupGitHubTokenOptions to accept and persist enterprise URL.
URL Utilities & API Configuration
src/lib/url.ts, src/lib/api-config.ts
Adds normalizeDomain(), githubBaseUrl(), and githubApiBaseUrl() utility functions for enterprise endpoint resolution. Converts GITHUB_API_BASE_URL and GITHUB_BASE_URL from constants to functions using state.enterpriseUrl. Adds enterprise-aware copilotBaseUrl logic.
GitHub & Copilot Service Layer
src/services/github/get-device-code.ts, src/services/github/poll-access-token.ts, src/services/github/get-user.ts, src/services/github/get-copilot-token.ts, src/services/github/get-copilot-usage.ts
Updates URL construction across service functions to call GITHUB_BASE_URL() and GITHUB_API_BASE_URL() as functions for dynamic enterprise routing.
Anthropic Message Handling
src/routes/messages/anthropic-types.ts, src/routes/messages/handler.ts
Adds signature: string property to AnthropicThinkingBlock. Introduces thinkingBlockOpen: boolean to AnthropicStreamState to track thinking block state during streaming.
Message Translation & Streaming
src/routes/messages/non-stream-translation.ts, src/routes/messages/stream-translation.ts
Replaces combined text+thinking content with separate reasoning_text and reasoning_opaque fields. Refactors streaming translation with modular handlers (handleMessageStart, handleThinkingText, handleContent, etc.). Adds thinking block signature extraction and content aggregation.
Public API Exports
src/services/copilot/create-chat-completions.ts
Exports Delta and Choice interfaces. Adds reasoning_text?: string | null and reasoning_opaque?: string | null fields to ResponseMessage and Message interfaces.
Token Processing
src/lib/tokenizer.ts
Skips token calculation for reasoning_opaque message field to exclude reasoning content from token counts.
Test Infrastructure
test-enterprise.sh
Adds Bash script for automated and manual enterprise testing, verifying CLI options, URL normalization, enterprise integration, file persistence, build, and typecheck status.
Test Suites
tests/url.test.ts, tests/copilot-base-url.test.ts, tests/enterprise-integration.test.ts, tests/enterprise-persistence.test.ts
Adds 41+ new tests: URL helper functions, Copilot API base URL routing by account type/enterprise, enterprise OAuth/API endpoint integration with mocked fetch, and enterprise URL persistence with file permissions.
Test Updates
tests/anthropic-request.test.ts, tests/anthropic-response.test.ts
Updates tests with signature fields in thinking blocks and thinkingBlockOpen: false in AnthropicStreamState initializations. Adjusts assertions to check reasoning_text instead of content for thinking data.

Sequence Diagram

sequenceDiagram
    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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Areas requiring extra attention:

  • src/routes/messages/non-stream-translation.ts — Complex refactoring of content aggregation: separation of reasoning content from text, signature extraction, and new helper function composition (getAnthropicThinkBlocks); requires careful review of content block ordering and reasoning field population across different paths (with/without tool calls, with/without images).
  • src/routes/messages/stream-translation.ts — Modular handler refactoring with new state transitions (handleMessageStart, handleThinkingText, closeThinkingBlockIfOpen, etc.); verify correct state machine progression and edge cases in chunk processing.
  • src/lib/api-config.ts — Conversion of GITHUB_API_BASE_URL and GITHUB_BASE_URL from constants to dynamic functions; verify all call sites correctly invoke as functions and that enterprise routing logic is applied consistently.
  • Public API changes (src/services/copilot/create-chat-completions.ts) — Delta and Choice interfaces now exported with new reasoning fields; ensure backward compatibility and that all consumers handle optional fields correctly.
  • Enterprise URL persistence flow (src/lib/token.ts, src/lib/paths.ts) — Verify file permission enforcement (600), correct read/write sequencing, and handling of edge cases (missing/corrupt files, undefined values).

Poem

🐰 Enterprise routes now bloom so bright,
Thinking blocks with signatures take flight—
GitHub clouds both public and private align,
Persistent URLs in storage by design.
Hops of code through enterprise skies,
Your multicloud dreams come to realize! ✨

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 3.70% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'Feat/enterprise and reasoning' is partially related to the changeset, referring to real aspects of the changes but lacks specificity and uses a vague format. Consider a more descriptive title like 'Add GitHub Enterprise support and reasoning fields to message API' that clearly conveys the main changes without relying on ambiguous terminology.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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.

  • Provide your own instructions using the high_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Use high_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a 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 consistent

Adding optional enterpriseUrl with a default of undefined is 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 schema

The added signature fields and reasoning_text assertions give good coverage for the new thinking-block translation behavior while preserving final text content and tool calls. Note that messageSchema used by isValidChatCompletionRequest does not declare reasoning_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 like reasoning_opaque) to messageSchema.

Also applies to: 169-201

tests/url.test.ts (1)

1-66: URL helper tests are solid; please confirm GHES API base pattern

The tests nicely exercise domain normalization and base URL helpers. For githubApiBaseUrl, you’re asserting https://api.ghe.example.com when an enterprise host like ghe.example.com is provided; if this helper is intended specifically for GitHub Enterprise Server, you may want to double-check whether your deployment expects an api.<host> subdomain versus the more common <host>/api/... pattern and adjust githubApiBaseUrl (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 fs module operations rather than testing the actual readEnterpriseUrl and writeEnterpriseUrl functions from src/lib/token.ts. Additionally, the URL normalization tests (lines 119-152) duplicate the normalization logic instead of calling normalizeDomain from src/lib/url.ts.

Consider refactoring to:

  1. Import and test readEnterpriseUrl and writeEnterpriseUrl from src/lib/token.ts
  2. Use normalizeDomain from src/lib/url.ts in normalization tests
  3. 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 configured
src/lib/url.ts (1)

1-4: Consider validating URL format after normalization.

The normalizeDomain function 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 "///" return undefined rather 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:

  1. Closes any open content block
  2. Emits reasoning opaque if the closed block wasn't a tool block
  3. Sends message_delta with stop_reason and usage
  4. Sends message_stop

The context.events.push on line 61 should use events directly 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

📥 Commits

Reviewing files that changed from the base of the PR and between 0ea08fe and 968ff12.

📒 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.ts
  • src/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.ts
  • src/routes/messages/stream-translation.ts
  • tests/anthropic-request.test.ts
  • src/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 Delta interface 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 Choice interface improves type reusability across the codebase.


114-120: LGTM!

Adding optional reasoning fields to ResponseMessage maintains consistency with the streaming types.


166-175: LGTM!

Adding optional reasoning fields to the Message interface allows reasoning content to be passed through the request/response chain.

src/routes/messages/anthropic-types.ts (2)

196-208: LGTM!

Adding thinkingBlockOpen to 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: string as required, not optional.

The codebase does contain defensive checks (lines 151-153 in non-stream-translation.ts check 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_URL from a constant to a function call correctly implements enterprise URL support, allowing the base URL to be computed dynamically based on state.enterpriseUrl.

src/services/github/get-user.ts (1)

6-6: LGTM!

Converting GITHUB_API_BASE_URL to 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 by githubBaseUrl(enterprise) in src/lib/url.ts
  • GitHub API (https://api.{enterprise}/...): Implemented by githubApiBaseUrl(enterprise) in src/lib/url.ts
  • Copilot (https://copilot-api.{enterprise}/...): Implemented by copilotBaseUrl(state) in src/lib/api-config.ts

Tests confirm all patterns function as documented.

src/services/github/get-copilot-token.ts (1)

7-7: Enterprise-aware API base usage looks correct

Switching to GITHUB_API_BASE_URL() keeps existing dotcom behavior while allowing enterprise routing via state.enterpriseUrl; no further changes needed here.

src/routes/messages/handler.ts (1)

58-64: Initialize thinkingBlockOpen in streaming state

Including thinkingBlockOpen: false in the initial AnthropicStreamState keeps 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 polling

Using GITHUB_BASE_URL() here is consistent with the new API config and ensures the /login/oauth/access_token endpoint 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 appropriately

Defining ENTERPRISE_URL_PATH and ensuring it is created with 0o600 permissions 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 URL

Calling GITHUB_BASE_URL() when constructing the /login/device/code URL 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 setting idleTimeout: 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: false field has been added to both test state initializations, consistent with the updated AnthropicStreamState type.

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 copilotBaseUrl behavior 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.com ensures 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-url flag, 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 readEnterpriseUrl helper properly handles missing files by catching errors and returning undefined. Trimming the content and returning undefined for empty strings is a good defensive pattern.


28-29: Consider awaiting or handling the write promise.

writeEnterpriseUrl returns 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 enterpriseUrl to 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 beforeEach correctly resets fetchCalls, clears enterpriseUrl, and sets up mock tokens. This ensures test isolation.


21-23: Proper cleanup restores original fetch.

Restoring globalThis.fetch in afterEach prevents 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 translateChunkToAnthropicEvents function 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:

  1. Closes thinking blocks before tool calls
  2. Handles reasoning opaque when transitioning from non-tool content
  3. Closes previous blocks before starting new tool use blocks
  4. 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.contentBlockOpen prevents duplicate block starts.


217-248: handleMessageStart initializes the stream correctly.

Emits message_start event with proper structure including model, usage, and initial state. The output_tokens: 0 with comment about later update is clear.


250-288: handleReasoningOpaque emits complete thinking blocks.

When reasoning_opaque exists 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:

  1. Opens thinking block on first thinking text
  2. Streams thinking_delta events
  3. 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, and get-copilot-token.ts) are correctly invoking GITHUB_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:

  1. Filters to non-empty thinking blocks
  2. Joins multiple thinking sections with double newlines
  3. 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 text type 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 > 0 prevents creating empty text blocks.


336-359: getAnthropicThinkBlocks handles all reasoning combinations.

The helper correctly handles:

  1. reasoning_text present → create thinking block with text and optional signature
  2. Only reasoning_opaque → create thinking block with empty text but signature
  3. 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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Suggested change
## [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.

Comment on lines +364 to +368
- 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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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.

Comment on lines +19 to +29
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`
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

Suggested change
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.

Comment on lines +76 to +78
if (key === "reasoning_opaque") {
continue
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 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=ts

Length 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).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants