feat(oxide-code): add Anthropic streaming client and async REPL - #2
Merged
Conversation
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.
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.
This comment was marked as outdated.
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.
4 tasks
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_*`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
config.rswith dual auth:ANTHROPIC_API_KEYenv var or Claude Code OAuth credentials from~/.claude/.credentials.jsonconfig/oauth.rswith proactive token refresh (5-minute buffer before expiry), directory-based file locking compatible with Claude Code'sproper-lockfile, and credential write-back preserving unknown JSON fieldsmessage.rswith conversation types:Role,ContentBlock(Text / ToolUse / ToolResult), andMessageclient/anthropic.rswith Anthropic Messages API streaming client — manual SSE parser, typedStreamEventdeserialization, channel-based streamingmain.rsas an async REPL: read stdin → call API → stream response to stdoutclaude-opus-4-6with 1M context beta headerKey Findings
"You are Claude Code, Anthropic's official CLI for Claude.") and theclaude-code-20250219beta header for the API to apply correct rate limits on non-Haiku models. Without these, Opus and Sonnet requests return 429.platform.claude.com/v1/oauth/token(JSON POST withgrant_type,refresh_token,client_id,scope).docs/research/anthropic-api.mdfor full details.Changes
crates/oxide-code/src/client.rscrates/oxide-code/src/client/anthropic.rscrates/oxide-code/src/config.rscrates/oxide-code/src/config/oauth.rscrates/oxide-code/src/message.rscrates/oxide-code/src/main.rscrates/oxide-code/Cargo.tomlCargo.tomlCLAUDE.mddocs/research/anthropic-api.mddocs/README.mddocs/roadmap.md.github/pull_request_template.md.markdownlint.jsonpyproject.tomlTest plan
cargo buildcompiles cleanlycargo clippy --all-targets -- -D warnings— zero warningscargo test— 23 tests pass (55.8% line coverage)ANTHROPIC_API_KEYcorrectly falls through to OAuth