Skip to content

feat(oxide-code): add Anthropic streaming client and async REPL - #2

Merged
hakula139 merged 35 commits into
mainfrom
feat/anthropic-client
Apr 2, 2026
Merged

feat(oxide-code): add Anthropic streaming client and async REPL#2
hakula139 merged 35 commits into
mainfrom
feat/anthropic-client

Conversation

@hakula139

@hakula139 hakula139 commented Apr 1, 2026

Copy link
Copy Markdown
Owner

Summary

Add the Anthropic Messages API streaming client, conversation types, and an async REPL — the foundation for an interactive AI coding assistant. Includes OAuth token refresh with proactive renewal, so sessions don't break when tokens expire.

  • Add config.rs with dual auth: ANTHROPIC_API_KEY env var or Claude Code OAuth credentials from ~/.claude/.credentials.json
  • Add config/oauth.rs with proactive token refresh (5-minute buffer before expiry), directory-based file locking compatible with Claude Code's proper-lockfile, and credential write-back preserving unknown JSON fields
  • Add message.rs with conversation types: Role, ContentBlock (Text / ToolUse / ToolResult), and Message
  • Add client/anthropic.rs with Anthropic Messages API streaming client — manual SSE parser, typed StreamEvent deserialization, channel-based streaming
  • Wire up main.rs as an async REPL: read stdin → call API → stream response to stdout
  • Default model set to claude-opus-4-6 with 1M context beta header
  • Add documentation index and Anthropic API authentication research notes

Key Findings

  • OAuth tokens require a system prompt prefix ("You are Claude Code, Anthropic's official CLI for Claude.") and the claude-code-20250219 beta header for the API to apply correct rate limits on non-Haiku models. Without these, Opus and Sonnet requests return 429.
  • Token refresh endpoint is platform.claude.com/v1/oauth/token (JSON POST with grant_type, refresh_token, client_id, scope).
  • See docs/research/anthropic-api.md for full details.

Changes

File Description
crates/oxide-code/src/client.rs Client module root
crates/oxide-code/src/client/anthropic.rs Anthropic Messages API streaming client with SSE parser
crates/oxide-code/src/config.rs Configuration loading (env vars, model, base URL, auth dispatch)
crates/oxide-code/src/config/oauth.rs OAuth credential loading, proactive token refresh, file locking
crates/oxide-code/src/message.rs Conversation message types (Role, ContentBlock, Message)
crates/oxide-code/src/main.rs Async REPL: stdin → API → streamed stdout
crates/oxide-code/Cargo.toml Crate dependencies
Cargo.toml Workspace dependency versions
CLAUDE.md Crate structure diagram and coding conventions
docs/research/anthropic-api.md Anthropic API authentication research notes
docs/README.md Documentation index
docs/roadmap.md Roadmap updates for current working features
.github/pull_request_template.md PR template with Changes table
.markdownlint.json Markdownlint configuration
pyproject.toml Python (uv) environment for dev scripts

Test plan

  • cargo build compiles cleanly
  • cargo clippy --all-targets -- -D warnings — zero warnings
  • cargo test — 23 tests pass (55.8% line coverage)
  • Smoke test: Opus 4.6 responds successfully via OAuth token
  • Empty ANTHROPIC_API_KEY correctly falls through to OAuth
  • Smoke test: token refresh triggers when token is near-expiry

anyhow, dirs, futures, reqwest (json + rustls-tls + stream),
serde, serde_json, tokio, tracing, tracing-subscriber.
Auth priority: ANTHROPIC_API_KEY env var > Claude Code OAuth
credentials at ~/.claude/.credentials.json.
Role, ContentBlock (Text / ToolUse / ToolResult), and Message
with convenience constructors.
SSE parser with typed StreamEvent deserialization. Supports both
API key and OAuth token auth via x-api-key header. Channel-based
streaming for non-blocking event consumption.
Simple stdin REPL that sends user input to the Anthropic API
and streams the response character-by-character to stdout.
@hakula139 hakula139 self-assigned this Apr 1, 2026
@hakula139 hakula139 added enhancement New feature or request labels Apr 1, 2026
hakula139 added 14 commits April 2, 2026 10:44
OAuth tokens require `Authorization: Bearer` + `anthropic-beta: oauth-2025-04-20`,
not `x-api-key`. Also clean up dead_code expect reasons that referenced internal
plan labels.
The API model ID is always `claude-opus-4-6` — the 1M context window
is activated by sending `anthropic-beta: context-1m-2025-08-07`.
Beta headers are now accumulated in a vec and joined, so OAuth
(`oauth-2025-04-20`) and 1M context compose cleanly.
Empty strings like `ANTHROPIC_API_KEY=""` were treated as valid values,
bypassing the OAuth fallback and causing auth failures.
The Anthropic API requires a specific system prompt prefix to identify
requests as Claude Code clients and apply correct rate limits for OAuth
tokens. Without it, non-Haiku models return 429.

Also add claude-code-20250219 and interleaved-thinking beta headers
that claude-code always sends, and the x-app: cli header.
Documents the OAuth authentication flow, required beta headers, system
prompt prefix requirement, and model ID conventions discovered by
reverse-engineering claude-code.
@hakula139

This comment was marked as outdated.

- Drop _returns_none / _returns_error test naming convention (not
  idiomatic Rust — use scenario-based names instead)
- Drop indoc! requirement
- Add visibility convention (prefer minimal: private → pub(crate) → pub)
- Add import ordering convention (std → external → internal)
- Add expect reason accuracy rule (describe current state, not future)
- Add unwrap/expect guidance (avoid in production, require invariant comment)
- Clarify pub use guidance (consistency over prohibition)
- Simplify helper ordering wording
- Simplify code review section (remove implementation-specific tooling)
- Fix #[expect(dead_code)] reasons: "consumed by agent loop" → "defined
  for full SSE protocol coverage" (no agent loop exists yet)
- Fix system prompt comment: clarify it's always sent, not OAuth-only
- Remove dead skip_serializing_if on system field (always Some)
- Rename tests to scenario-based names per revised CLAUDE.md
- Reorder tests: happy path → variants → edge cases
- Convert escaped test strings to raw strings
Move credentials_path below load_claude_oauth per CLAUDE.md convention
that helper functions follow their caller.
Group always-sent beta headers together before conditional OAuth
header, matching the vec initialization order in Client::new.
Add indoc as a dev dependency and convert SSE frame test literals
to use indoc! macro, keeping test data properly indented with the
surrounding code. Add convention to CLAUDE.md.
- Fix claude-code version (v2.1.87 → v2.1.88) and link to fork
- Remove subscription-specific fields from credential example
- Expand "1P" to "first-party only" in beta header table
- Reorder beta headers by adoption priority
- Update token refresh URL to platform.claude.com
- Document oxide-code's refresh implementation and add source refs
- Extract OAuth logic from config.rs into config/oauth.rs
- Refresh tokens proactively with a 5-minute buffer before expiry
- Use directory-based locking (~/.claude.lock) compatible with Claude
  Code's proper-lockfile for cross-process safety
- Double-check credentials after acquiring lock (another process may
  have already refreshed)
- Graceful degradation: if refresh fails but token is still valid, warn
  and use existing token
- Write back refreshed credentials preserving unknown JSON fields
- Set 0o600 permissions on credentials file after write
- Make Config::load() async to support the refresh HTTP call
- Replace Follow-ups section with per-file Changes table in PR template
- Add prose intro guidance and markdownlint suppression
- Reorganize git conventions into Commits / Branches / Pull Requests
- Narrow commit scope guidance to module-level (not crate-level)
…oken

Destructure creds.claude_ai_oauth into a local `oauth` binding and add
OAuthCredential::expires_at_ms() to avoid repeating the u64 conversion.
Import Client, Delta, and StreamEvent all from client::anthropic
instead of mixing re-exported and direct paths.
Also: replace .expect() with .context()? in stream_message, add doc
comment on write_refreshed_credentials lock precondition, document
clock skew assumption on expiresAt, and sort OAUTH_SCOPES alphabetically.
@hakula139
hakula139 merged commit 8ec6f56 into main Apr 2, 2026
1 check passed
@hakula139
hakula139 deleted the feat/anthropic-client branch April 2, 2026 08:02
hakula139 added a commit that referenced this pull request Apr 29, 2026
Decision #4 in the design section claimed `truncated_total` would
become the single structural signal; the PR ended up splitting into
`truncated_total` (view-shape) + `truncated_bytes` (byte cap) after
review caught a unit-conflation hazard. Notes now describe the split
and the rationale.

Source-line list also updated: the bash and read self-cap references
are gone with the code; remaining entries point at the constants and
helpers without brittle line ranges. Test-name references in
decision #2 follow the rename from `truncate_output_*` to
`cap_output_*`.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant