Skip to content

feat: flare-proxy crate — Anthropic to OpenAI free-provider proxy - #261

Merged
getappz merged 8 commits into
masterfrom
feat/flare-proxy
Jul 19, 2026
Merged

feat: flare-proxy crate — Anthropic to OpenAI free-provider proxy#261
getappz merged 8 commits into
masterfrom
feat/flare-proxy

Conversation

@getappz

@getappz getappz commented Jul 19, 2026

Copy link
Copy Markdown
Owner

Summary

  • New crates/flare-proxy/: proxies /proxy/v1/messages (Anthropic Messages API) to free OpenAI-compatible providers (NVIDIA NIM, OpenRouter, LM Studio), mounted on the existing agentflare serve dashboard server.
  • Includes request/response/stream translation (shape_xlat.rs), heuristic tool-call extraction for providers without native function calling, and <think> tag stripping.
  • Reviewed on item Adopt #7: dev-install — atomic build-and-replace #209; 6 fixes applied on top of the original implementation before merge:
    • NVIDIA NIM Authorization header was missing the Bearer scheme, breaking the default free-tier route entirely.
    • SSE content-block index collision + unclosed blocks for native tool-calling (text block and first tool call both claimed index 0; tool blocks never got content_block_stop). Buffer now tracks open indices and allocates fresh ones.
    • Heuristic tool-call block was being injected into the SSE stream after message_stop — reordered so it closes before the message ends.
    • SSE parsing now buffers partial lines across TCP chunk boundaries instead of silently dropping truncated JSON.
    • /proxy/v1/messages gated behind an opt-in AGENTFLARE_PROXY_TOKEN shared-secret check (dashboard server can be bound off-localhost).
    • Error responses now carry Content-Type: application/json; heuristic tool-call IDs use the workspace's existing nanoid crate instead of a hand-rolled generator.

Test plan

  • cargo test -p flare-proxy — 12/12 pass (11 original + 1 new regression test for the index-collision fix)
  • cargo clippy -p flare-proxy --all-features -- -D warnings — clean
  • cargo build --workspace — clean (1 pre-existing unrelated warning in src/components.rs)
  • cargo test --bin agentflare dashboard — 11/11 pass, confirms dashboard server wiring unaffected
  • Manual smoke test against a live NVIDIA NIM/OpenRouter key (not run in this session)

Summary by CodeRabbit

  • New Features
    • Added a new proxy endpoint that translates Anthropic-style chat requests into OpenAI-compatible provider calls.
    • Enabled streaming responses as Server-Sent Events with chunk buffering.
    • Added provider/model routing with a built-in default configuration.
    • Added best-effort heuristic tool-call extraction and optional <think>...</think> stripping.
    • Added optional token-based request authorization and integrated proxy routes into the dashboard.
  • Bug Fixes
    • Improved upstream non-success handling by returning translated, JSON-formatted error responses.
  • Tests
    • Added unit coverage for request/response translation, streaming behavior, and heuristic tool-call extraction.

@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 6942f6e8-c27b-4a9f-8b41-839d2a2a66de

📥 Commits

Reviewing files that changed from the base of the PR and between 607c977 and 4950368.

📒 Files selected for processing (5)
  • crates/flare-proxy/src/forward.rs
  • crates/flare-proxy/src/heuristic.rs
  • crates/flare-proxy/src/lib.rs
  • crates/flare-proxy/src/shape_xlat.rs
  • crates/flare-proxy/src/think.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • crates/flare-proxy/src/think.rs
  • crates/flare-proxy/src/heuristic.rs
  • crates/flare-proxy/src/shape_xlat.rs

📝 Walkthrough

Walkthrough

Adds the flare-proxy Rust crate, provider routing, Anthropic/OpenAI translation, streaming SSE conversion, heuristic output parsing, authorization, and dashboard router integration.

Changes

Flare proxy

Layer / File(s) Summary
Provider contracts and crate wiring
Cargo.toml, crates/flare-proxy/Cargo.toml, crates/flare-proxy/src/providers.rs, crates/flare-proxy/src/lib.rs
Registers the crate, defines provider and model routes, and initializes the Axum proxy router with shared state.
Anthropic and OpenAI shape translation
crates/flare-proxy/src/shape_xlat.rs
Converts request, response, tool, error, and streaming payloads between Anthropic and OpenAI formats, with translation tests.
Heuristic tool and think parsing
crates/flare-proxy/src/heuristic.rs, crates/flare-proxy/src/think.rs
Extracts free-form tool calls, strips think tags, detects affected models, and tests parsing behavior.
Proxy request execution and routing integration
crates/flare-proxy/src/forward.rs, crates/flare-proxy/src/lib.rs, src/dashboard/server.rs
Adds token authorization, provider request forwarding, upstream error handling, translated SSE streaming, and dashboard route mounting.

Estimated code review effort: 4 (Complex) | ~45 minutes

Possibly related PRs

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant v1_messages_handler
  participant ProviderConfig
  participant proxy_request
  participant reqwest
  Client->>v1_messages_handler: POST /proxy/v1/messages
  v1_messages_handler->>ProviderConfig: resolve model route and provider
  v1_messages_handler->>proxy_request: forward Anthropic JSON
  proxy_request->>reqwest: POST upstream /chat/completions
  reqwest-->>proxy_request: streaming OpenAI SSE chunks
  proxy_request-->>Client: translated Anthropic SSE response
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.54% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: adding a flare-proxy crate for Anthropic-to-OpenAI provider proxying.
Description check ✅ Passed The description covers Summary and Test plan well, but it omits the template's Notes for reviewers section details.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/flare-proxy

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 10

🧹 Nitpick comments (3)
src/dashboard/server.rs (1)

230-230: 🔒 Security & Privacy | 🔵 Trivial

Merge looks correct; operational reminder.

Route mounting and ordering relative to fallback(static_handler) is correct. Operationally: this endpoint spends the operator's provider quota and is now reachable wherever the dashboard is bound (including non-localhost, per the existing warning further down this file) unless AGENTFLARE_PROXY_TOKEN is set — worth documenting that operators binding off-localhost should set this env var.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/dashboard/server.rs` at line 230, Document near the dashboard
bind-address warning or proxy route setup that operators exposing the dashboard
beyond localhost should set AGENTFLARE_PROXY_TOKEN, since the mounted
flare_proxy route consumes provider quota and is otherwise publicly reachable.
crates/flare-proxy/src/heuristic.rs (1)

31-73: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick win

Regexes recompiled on every call.

extract_invoke_meal, extract_code_fence_json, and extract_json_tool_block each call Regex::new inline every time try_extract_tool_call runs, which is a well-known Rust anti-pattern (compilation isn't free). Prefer compiling once via std::sync::LazyLock/once_cell::sync::Lazy.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/flare-proxy/src/heuristic.rs` around lines 31 - 73, The regexes in
extract_invoke_meal, extract_code_fence_json, and extract_json_tool_block are
recompiled on every invocation. Define each pattern once using
std::sync::LazyLock or the project’s established once_cell::sync::Lazy approach,
then reuse the initialized regexes inside these functions while preserving their
existing matching behavior.
crates/flare-proxy/src/shape_xlat.rs (1)

187-251: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

Remove or wire up chat_to_messages
No runtime caller in this crate references it; only the unit tests do. If there’s no external use, drop it to avoid dead code and drift from the streaming translation path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/flare-proxy/src/shape_xlat.rs` around lines 187 - 251, Remove the
unused chat_to_messages function and its associated tests, since no runtime code
in the crate calls it. Verify imports or helpers used only by this function are
also removed, while leaving the streaming translation path unchanged.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@crates/flare-proxy/src/forward.rs`:
- Around line 179-188: The current post-processing block in the streaming flow
only computes stripped text after raw deltas have already been emitted, so
requires_think_parsing routes still expose think tags. Update the logic around
openai_chunk_to_anthropic_sse and strip_think_tags to remove or transform
think-tag content before sending each client-visible SSE event, preserving
non-thought text and the existing route behavior; do not rely on the discarded
_clean result after is_finish.
- Around line 61-64: Update the request flow around reqwest::Client in the
forwarding function to reuse a client stored in AppState instead of constructing
one per call. Configure that shared client with an explicit timeout during
AppState initialization, then use it for the existing POST request while
preserving the current request payload and endpoint.
- Around line 104-120: Update the SSE buffering logic in the stream closure
around line_buf so raw chunk bytes are accumulated before UTF-8 decoding. Track
complete lines at the byte level, decode only each assembled complete line after
removing it from the byte buffer, and preserve partial multibyte sequences and
trailing partial lines across chunks.

In `@crates/flare-proxy/src/heuristic.rs`:
- Around line 98-104: Update needs_heuristic_tools to normalize the model
identifier to a consistent case before checking the existing model-family
substrings, so mixed-case identifiers trigger the same heuristic detection as
lowercase values. Preserve the current substring list and boolean behavior.
- Around line 56-94: Fix extract_json_tool_block by slicing from the regex
match’s opening-brace position rather than from the "name" field, so
find_balanced_brace receives the complete outer object and serde_json can parse
it. Update find_balanced_brace to ignore braces occurring inside JSON string
literals, including escaped quotes, and add coverage for bare JSON tool blocks
and brace characters inside string values.
- Around line 31-54: Update the regex patterns in extract_invoke_meal and
extract_code_fence_json to enable dot-all matching so their .*? sections span
newlines, preserving existing capture behavior. Add or update tests to verify
both extractors parse pretty-printed multiline JSON payloads.

In `@crates/flare-proxy/src/lib.rs`:
- Around line 34-42: Update the proxy token validation around
AGENTFLARE_PROXY_TOKEN so a configured empty value cannot disable
authentication: treat an empty expected token as invalid configuration or reject
requests rather than comparing it against the missing-header default. Preserve
the existing missing/invalid token response, and use constant-time token
comparison if the project already provides suitable support.

In `@crates/flare-proxy/src/shape_xlat.rs`:
- Around line 74-124: Update translate_user_content to extract data and
tool_use_id as strings before formatting, avoiding JSON-quoted values in image
URLs and tool-result markers. Validate the text field with the same ?-based
string extraction pattern used for other fields, and skip blocks whose text is
missing or not a string instead of serializing null.
- Around line 8-10: Update the Anthropic system-prompt handling in the
messages-to-chat conversion to prepend a system-role message with the prompt
content to the front of messages, rather than assigning body["system"]. Preserve
existing message conversion, and update test_messages_to_chat_with_system to
assert the prepended message shape.

In `@crates/flare-proxy/src/think.rs`:
- Around line 27-29: Update needs_think_parsing to use the shared model-family
detection logic from heuristic.rs, avoiding duplicated case-sensitive substring
checks and ensuring the supported families include mistral and mixtral alongside
the existing families.

---

Nitpick comments:
In `@crates/flare-proxy/src/heuristic.rs`:
- Around line 31-73: The regexes in extract_invoke_meal,
extract_code_fence_json, and extract_json_tool_block are recompiled on every
invocation. Define each pattern once using std::sync::LazyLock or the project’s
established once_cell::sync::Lazy approach, then reuse the initialized regexes
inside these functions while preserving their existing matching behavior.

In `@crates/flare-proxy/src/shape_xlat.rs`:
- Around line 187-251: Remove the unused chat_to_messages function and its
associated tests, since no runtime code in the crate calls it. Verify imports or
helpers used only by this function are also removed, while leaving the streaming
translation path unchanged.

In `@src/dashboard/server.rs`:
- Line 230: Document near the dashboard bind-address warning or proxy route
setup that operators exposing the dashboard beyond localhost should set
AGENTFLARE_PROXY_TOKEN, since the mounted flare_proxy route consumes provider
quota and is otherwise publicly reachable.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 419c4cd1-4a0f-47ce-9a2f-f38b66c9c4f8

📥 Commits

Reviewing files that changed from the base of the PR and between 3e2c400 and d5caa4a.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (9)
  • Cargo.toml
  • crates/flare-proxy/Cargo.toml
  • crates/flare-proxy/src/forward.rs
  • crates/flare-proxy/src/heuristic.rs
  • crates/flare-proxy/src/lib.rs
  • crates/flare-proxy/src/providers.rs
  • crates/flare-proxy/src/shape_xlat.rs
  • crates/flare-proxy/src/think.rs
  • src/dashboard/server.rs

Comment thread crates/flare-proxy/src/forward.rs Outdated
Comment thread crates/flare-proxy/src/forward.rs
Comment thread crates/flare-proxy/src/forward.rs Outdated
Comment on lines +179 to +188
// Think tag stripping on accumulated text
if needs_think && !accumulated_text.is_empty() {
let (_clean, thoughts) = crate::think::strip_think_tags(&accumulated_text);
if !thoughts.is_empty() {
// We've already streamed the text with think tags.
// In a real implementation, we'd buffer and re-stream.
// For v1, we strip in post-processing of accumulated text.
// The SSE events already went out; this is best-effort cleanup.
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Think-tag stripping is a no-op on the actual streamed output.

Raw text deltas (including <think> tags) are already emitted via openai_chunk_to_anthropic_sse before is_finish is reached. The strip_think_tags call here only computes thoughts from accumulated_text and discards it — for requires_think_parsing: true routes, think-tag content will still leak into the client-visible stream, contrary to what the route flag implies.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/flare-proxy/src/forward.rs` around lines 179 - 188, The current
post-processing block in the streaming flow only computes stripped text after
raw deltas have already been emitted, so requires_think_parsing routes still
expose think tags. Update the logic around openai_chunk_to_anthropic_sse and
strip_think_tags to remove or transform think-tag content before sending each
client-visible SSE event, preserving non-thought text and the existing route
behavior; do not rely on the discarded _clean result after is_finish.

Comment thread crates/flare-proxy/src/heuristic.rs
Comment thread crates/flare-proxy/src/heuristic.rs
Comment thread crates/flare-proxy/src/heuristic.rs
Comment thread crates/flare-proxy/src/lib.rs
Comment thread crates/flare-proxy/src/shape_xlat.rs
Comment thread crates/flare-proxy/src/shape_xlat.rs
Comment on lines +27 to +29
pub fn needs_think_parsing(model: &str) -> bool {
model.contains("deepseek") || model.contains("qwen") || model.contains("llama")
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Model-family detection is case-sensitive and duplicated from heuristic.rs.

Same lowercase-substring pattern as needs_heuristic_tools in heuristic.rs, but with a different list (missing "mistral"/"mixtral" here). See consolidated comment.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/flare-proxy/src/think.rs` around lines 27 - 29, Update
needs_think_parsing to use the shared model-family detection logic from
heuristic.rs, avoiding duplicated case-sensitive substring checks and ensuring
the supported families include mistral and mixtral alongside the existing
families.

getappz added 3 commits July 19, 2026 16:01
cargo-deny licenses failed: flare-proxy had no license field, unlike
every other workspace crate. clippy --locked failed: Cargo.lock was
never regenerated after nanoid was added as a dependency.
- shape_xlat: prepend system prompt as a system message instead of
  setting body[\system\], which the OpenAI chat-completions schema
  does not have; system prompts were being silently dropped on every
  request (critical - breaks the proxy for real agent traffic).
- shape_xlat: translate_user_content stringified image data and
  tool_use_id via Value's Display impl, embedding JSON quotes into
  base64 data URLs and tool_result markers; extract as str instead.
- heuristic: extract_json_tool_block sliced from the \name\ substring
  instead of the enclosing brace, so find_balanced_brace matched the
  nested arguments object and the extractor always returned None; slice
  from the regex match start instead. find_balanced_brace now also
  tracks string state so braces inside string values do not miscount.
- heuristic/think: add (?s) so .*? spans newlines, matching
  pretty-printed JSON tool calls; make model-family detection
  case-insensitive; think.rs now also matches mistral/mixtral.
- lib: reject a configured-but-empty AGENTFLARE_PROXY_TOKEN instead of
  silently disabling the auth gate.
- forward: reuse one reqwest::Client with a timeout via AppState
  instead of building a new client per request; buffer raw bytes
  across SSE chunk boundaries before UTF-8 decoding instead of
  lossy-decoding each raw chunk, which could mangle a multibyte
  sequence split across a chunk boundary.

Think-tag suppression during streaming is left as a known limitation
(flagged Heavy lift by review): raw deltas are already streamed before
the accumulated-text pass runs, so it cannot retroactively strip tags
from what the client already received. Fixing that requires buffering
deltas and delaying emission, tracked separately.
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.

1 participant