Print warning if not using anthropic subscription (#713)

* ##  Implementation Complete!

I've successfully implemented the warning system for Claude Code when `apiKeySource` is not "/login managed key". Here's what was implemented:

### **Changes Made:**

1. **Added `api_key_source` field to `ClaudeJson::System`**
   - Added `#[serde(default, rename = "apiKeySource")] api_key_source: Option<String>` field
   - Backward-compatible with existing JSON parsing

2. **Created warning helper function**
   - `warn_if_unmanaged_key()` function that generates warning entries when API key source is not managed
   - Includes both UI warning and server-side `tracing::warn!()` logging

3. **Modified system message processing**
   - Updated `normalize_entries()` to check API key source and emit warnings immediately
   - Warning appears for any `apiKeySource` value except "/login managed key" and `None`

4. **Added comprehensive test coverage**
   - Test for warning generation with unmanaged keys
   - Test for no warning with managed keys
   - Test for no warning when field is missing
   - All existing tests pass

### **Warning Message:**
```
⚠️  Using apiKeySource "ANTHROPIC_API_KEY" – calls will be billed to that key. Run `claude-code login` (or set `/login managed key`) if you want to route usage through the managed key.
```

### **Key Features:**
- **Immediate visibility**: Warning shows up immediately in the UI as a normalized system message
- **Server-side logging**: Also logs to server with `tracing::warn!()`
- **Backward compatible**: No breaking changes, works with existing implementations
- **Comprehensive**: Handles all cases (managed, unmanaged, missing field)
- **Actionable**: Provides clear guidance on how to fix the issue

The implementation is ready for production use!

* Cleanup script changes for task attempt 4345b766-b8ed-4949-aefb-32e20e891a01

* ##  Review Feedback Addressed!

I've successfully updated the implementation based on your review comments:

### **Changes Made:**

1. **Updated warning logic (Line 621)**
   - Changed from checking "not `/login managed key`" to specifically checking for `"ANTHROPIC_API_KEY"`
   - Now only warns when `apiKeySource` equals exactly `"ANTHROPIC_API_KEY"`
   - All other values (including `/login managed key`, missing field, or other keys) do not trigger warnings

2. **Updated warning message (Line 631)**
   - Changed from the generic billing warning to the specific message:
   - **New message:** `"ANTHROPIC_API_KEY env variable detected, your Anthropic subscription is not being used"`

3. **Updated tests to reflect new behavior**
   - Tests now verify warning only appears for `"ANTHROPIC_API_KEY"`
   - Tests confirm no warning for managed keys, other keys, or missing field
   - Validates exact warning message content

### **Final Behavior:**
-  **Warning shown**: When `apiKeySource` = `"ANTHROPIC_API_KEY"`
-  **No warning**: When `apiKeySource` = `"/login managed key"`
-  **No warning**: When `apiKeySource` = any other value
-  **No warning**: When `apiKeySource` field is missing

The implementation now specifically targets the `ANTHROPIC_API_KEY` environment variable scenario and provides a clear, focused warning message about Anthropic subscription usage.

* Cleanup script changes for task attempt 4345b766-b8ed-4949-aefb-32e20e891a01

* ##  Warning Triangle Added!

I've successfully added the warning triangle (⚠️) to the warning message as requested.

### **Final Warning Message:**
```
⚠️ ANTHROPIC_API_KEY env variable detected, your Anthropic subscription is not being used
```

### **Changes Made:**
- **Line 628**: Added `⚠️` to the warning message content
- **Updated test**: Modified the test assertion to expect the warning triangle in the message

The warning now has a clear visual indicator (⚠️) that makes it more noticeable to users when the `ANTHROPIC_API_KEY` environment variable is being used instead of their Anthropic subscription through Claude Code.

The implementation is complete and ready for production use!
This commit is contained in:
Louis Knight-Webb
2025-09-15 13:07:32 +01:00
committed by GitHub
parent 47dc2cd78b
commit c6407c64ac

View File

@@ -615,6 +615,24 @@ impl ClaudeLogProcessor {
}
}
/// Generate warning entry if API key source is ANTHROPIC_API_KEY
fn warn_if_unmanaged_key(src: &Option<String>) -> Option<NormalizedEntry> {
match src.as_deref() {
Some("ANTHROPIC_API_KEY") => {
tracing::warn!(
"ANTHROPIC_API_KEY env variable detected, your Anthropic subscription is not being used"
);
Some(NormalizedEntry {
timestamp: None,
entry_type: NormalizedEntryType::SystemMessage,
content: "⚠️ ANTHROPIC_API_KEY env variable detected, your Anthropic subscription is not being used".to_string(),
metadata: None,
})
}
_ => None,
}
}
/// Convert Claude JSON to normalized entries
fn normalize_entries(
&mut self,
@@ -622,25 +640,52 @@ impl ClaudeLogProcessor {
worktree_path: &str,
) -> Vec<NormalizedEntry> {
match claude_json {
ClaudeJson::System { subtype, .. } => {
let content = match subtype.as_deref() {
ClaudeJson::System {
subtype,
api_key_source,
..
} => {
let mut entries = Vec::new();
// 1) emit billing warning if required
if let Some(warning) = Self::warn_if_unmanaged_key(api_key_source) {
entries.push(warning);
}
// 2) keep the existing behaviour for the normal system message
match subtype.as_deref() {
Some("init") => {
// Skip system init messages because it doesn't contain the actual model that will be used in assistant messages in case of claude-code-router.
// We'll send system initialized message with first assistant message that has a model field.
return vec![];
return entries; // only the warning (if any)
}
Some(subtype) => format!("System: {subtype}"),
None => "System message".to_string(),
};
Some(subtype) => {
let content = format!("System: {subtype}");
entries.push(NormalizedEntry {
timestamp: None,
entry_type: NormalizedEntryType::SystemMessage,
content,
metadata: Some(
serde_json::to_value(claude_json)
.unwrap_or(serde_json::Value::Null),
),
});
}
None => {
let content = "System message".to_string();
entries.push(NormalizedEntry {
timestamp: None,
entry_type: NormalizedEntryType::SystemMessage,
content,
metadata: Some(
serde_json::to_value(claude_json)
.unwrap_or(serde_json::Value::Null),
),
});
}
}
vec![NormalizedEntry {
timestamp: None,
entry_type: NormalizedEntryType::SystemMessage,
content,
metadata: Some(
serde_json::to_value(claude_json).unwrap_or(serde_json::Value::Null),
),
}]
entries
}
ClaudeJson::Assistant { message, .. } => {
let mut entries = Vec::new();
@@ -1061,6 +1106,8 @@ pub enum ClaudeJson {
cwd: Option<String>,
tools: Option<Vec<serde_json::Value>>,
model: Option<String>,
#[serde(default, rename = "apiKeySource")]
api_key_source: Option<String>,
},
#[serde(rename = "assistant")]
Assistant {
@@ -1771,6 +1818,45 @@ mod tests {
assert_eq!(entries.len(), 0);
}
#[test]
fn test_api_key_source_warning() {
// Test with ANTHROPIC_API_KEY - should generate warning
let system_with_env_key = r#"{"type":"system","subtype":"init","apiKeySource":"ANTHROPIC_API_KEY","session_id":"test123"}"#;
let parsed: ClaudeJson = serde_json::from_str(system_with_env_key).unwrap();
let entries = ClaudeLogProcessor::new().normalize_entries(&parsed, "");
assert_eq!(entries.len(), 1);
assert!(matches!(
entries[0].entry_type,
NormalizedEntryType::SystemMessage
));
assert_eq!(
entries[0].content,
"⚠️ ANTHROPIC_API_KEY env variable detected, your Anthropic subscription is not being used"
);
// Test with managed API key source - should not generate warning
let system_with_managed_key = r#"{"type":"system","subtype":"init","apiKeySource":"/login managed key","session_id":"test123"}"#;
let parsed_managed: ClaudeJson = serde_json::from_str(system_with_managed_key).unwrap();
let entries_managed = ClaudeLogProcessor::new().normalize_entries(&parsed_managed, "");
assert_eq!(entries_managed.len(), 0); // No warning for managed key
// Test with other apiKeySource values - should not generate warning
let system_other_key = r#"{"type":"system","subtype":"init","apiKeySource":"OTHER_KEY","session_id":"test123"}"#;
let parsed_other: ClaudeJson = serde_json::from_str(system_other_key).unwrap();
let entries_other = ClaudeLogProcessor::new().normalize_entries(&parsed_other, "");
assert_eq!(entries_other.len(), 0); // No warning for other keys
// Test with missing apiKeySource - should not generate warning
let system_no_key = r#"{"type":"system","subtype":"init","session_id":"test123"}"#;
let parsed_no_key: ClaudeJson = serde_json::from_str(system_no_key).unwrap();
let entries_no_key = ClaudeLogProcessor::new().normalize_entries(&parsed_no_key, "");
assert_eq!(entries_no_key.len(), 0); // No warning when field is missing
}
#[test]
fn test_mixed_content_with_thinking_ignores_tool_result() {
let complex_assistant_json = r#"{"type":"assistant","message":{"role":"assistant","content":[{"type":"thinking","thinking":"I need to read the file first"},{"type":"text","text":"I'll help you with that"},{"type":"tool_result","tool_use_id":"tool_789","content":"Success","is_error":false}]}}"#;