0.99.57

Bump version to 0.99.57; add Claude CLI proxy mode

- Add aisbf/cli_mode.py: detect claude binary in PATH at startup
- Add ClaudeCliSessionManager: per-user isolated temp config dirs with
  10-minute idle cleanup and asyncio subprocess-based request proxying
- Add ClaudeProviderHandler CLI methods: _get_cli_credentials,
  _messages_to_cli_prompt, _handle_cli_streaming_request,
  _handle_cli_request, _oauth_tokens_to_cli_credentials
- Wire CLI mode check into _handle_request_with_model; falls through to
  HTTP API mode when no CLI credentials are configured
- Add 'Use Claude CLI mode' checkbox in provider config (both admin and
  user templates): derives credentials from existing OAuth2 tokens
- Add explicit CLI credentials file upload (file_type=cli_credentials);
  DB users: stored in user_oauth2_credentials; admin: path in providers.json
- Update Claude provider warning notices to scope risk to HTTP API mode
  and clarify that claude -p is permitted by Claude's terms of service
- Update CHANGELOG.md, DOCUMENTATION.md, README.md
parent 8fed50ce
......@@ -7,6 +7,40 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.99.57] - 2026-04-23
### Added
- **Claude CLI Mode** (`aisbf/cli_mode.py`, `aisbf/providers/claude.py`)
- At startup, AISBF checks for the `claude` binary in PATH via `shutil.which`; if found, CLI proxy mode is enabled globally
- New `ClaudeCliSessionManager` class manages per-user isolated temporary config directories (`CLAUDE_CONFIG_DIR`) containing `settings.json` (onboarding bypass) and `.credentials.json`
- Session directories are reused across requests and automatically cleaned up after 10 minutes of idle time; the background cleanup task runs every 60 seconds
- Each request spawns a dedicated `asyncio.create_subprocess_exec` subprocess running `claude -p --input-format stream-json --output-format stream-json --tools "" --dangerously-skip-permissions --no-session-persistence --verbose --model <model>`; multiple parallel requests are fully non-blocking
- Streaming output is parsed from the CLI's `stream-json` format and re-emitted as OpenAI-compatible SSE chunks
- Non-streaming requests collect all `text_delta` events and return a standard `chat.completion` response
- Messages are converted to a flat text prompt (system instructions as prefix, then `Human:`/`Assistant:` turns) — no Anthropic billing-header injection needed in CLI mode
- CLI mode falls through gracefully to HTTP API mode if no CLI credentials are configured for the requesting user
- **"Use Claude CLI mode" checkbox** (provider configuration UI)
- New `claude_config.use_cli_mode` boolean field in the Claude provider configuration
- When checked, AISBF derives CLI credentials automatically from the user's existing OAuth2 tokens already stored in AISBF, converting them to the `claudeAiOauth` schema expected by the CLI (`expires_at` seconds → `expiresAt` milliseconds, `scope` string → `scopes` list)
- No separate file upload required when OAuth2 authentication has already been completed
- Available in both admin (`providers.html`) and user (`user_providers.html`) provider configuration pages, shown only when the `claude` binary is detected at startup
- **Explicit CLI credentials file upload**
- Users can upload their own `~/.claude/.credentials.json` to use a specific Claude account independently of their AISBF OAuth2 session
- Uploaded file takes priority over OAuth2-derived credentials
- Admin: file path saved in `providers.json` under `claude_config.cli_credentials_file`
- Database users (all roles): credentials JSON content stored in `user_oauth2_credentials` with `auth_type='claude_cli_credentials'`; temp file removed after content is persisted to DB
- **`ClaudeProviderHandler._oauth_tokens_to_cli_credentials()`** static method
- Converts AISBF's internal OAuth2 token dict to the `claudeAiOauth` credentials schema used by the Claude CLI
- Handles missing `scope` by defaulting to the standard five-scope set: `user:inference`, `user:file_upload`, `user:profile`, `user:mcp_servers`, `user:sessions:claude_code`
### Changed
- **Claude provider warning notices** updated in both `providers.html` and `user_providers.html`
- Warning now explicitly scopes the "unofficial client" risk to HTTP API / OAuth2 mode
- Added a green ✅ note clarifying that CLI mode uses the official `claude -p` interface, which is the intended programmatic use of the Anthropic CLI and is permitted by Claude's terms of service
## [0.99.49] - 2026-04-21
### Added
......
......@@ -6,9 +6,10 @@ AISBF is a modular proxy server for managing multiple AI provider integrations.
### Key Features
- **Multi-Provider Support**: Unified interface for Google, OpenAI, Anthropic, Claude Code (OAuth2), Ollama, Kiro (Amazon Q Developer), Kilocode (OAuth2), Codex (OAuth2), and Qwen (API Key/OAuth2)
- **Multi-Provider Support**: Unified interface for Google, OpenAI, Anthropic, Claude Code (OAuth2 or CLI), Ollama, Kiro (Amazon Q Developer), Kilocode (OAuth2), Codex (OAuth2), and Qwen (API Key/OAuth2)
- **Unified Wallet System**: Fiat wallet with crypto/PayPal/Stripe top-ups and auto top-up for subscription renewals
- **Claude OAuth2 Authentication**: Full OAuth2 PKCE flow for Claude Code with automatic token refresh, Chrome extension for remote servers, and curl_cffi TLS fingerprinting support
- **Claude CLI Mode**: When the `claude` binary is present in PATH at startup, AISBF automatically enables CLI proxy mode — requests are piped through the official Anthropic CLI (`claude -p`) instead of the HTTP API, using per-user isolated config directories with a 10-minute idle cleanup
- **Kiro-cli Support**: Full support for Amazon Q Developer CLI authentication with Device Authorization Grant
- **Kilocode OAuth2 Authentication**: OAuth2 Device Authorization Grant for Kilo Code with automatic token refresh
- **Codex OAuth2 Authentication**: OAuth2 Device Authorization Grant for OpenAI Codex with automatic token refresh and API key exchange
......@@ -519,12 +520,46 @@ AISBF automatically extracts and tracks model metadata from provider responses:
- Static model list (no dynamic model discovery)
- cache_control support for cost reduction
### Claude Code (OAuth2)
### Claude Code (OAuth2 / CLI)
AISBF supports two modes for the Claude provider, selected automatically at runtime:
#### HTTP API / OAuth2 mode (default)
- Full OAuth2 PKCE authentication flow
- Automatic token refresh with refresh token rotation
- Chrome extension for remote server OAuth2 callback interception
- Proxy-aware extension serving: automatically detects reverse proxy deployments
- Supports all Claude models with streaming, tool calling, vision, and extended thinking
- **Note**: this mode uses an unofficial client interface; use at your own risk as per Claude's terms of service
#### CLI proxy mode (requires `claude` in PATH)
- Activated automatically at startup when the `claude` binary is found in PATH
- Requests are proxied through the official Anthropic CLI using `claude -p --input-format stream-json --output-format stream-json`
- Uses the official CLI as intended by Anthropic — permitted by Claude's terms of service
- Per-user isolated temporary config directories (`CLAUDE_CONFIG_DIR`) prevent credential cross-contamination
- Idle session cleanup: temp dirs are removed after 10 minutes of inactivity; active requests always get a fresh subprocess
- Multiple parallel requests are handled concurrently via `asyncio.create_subprocess_exec`
- Credentials can be supplied in two ways:
- **"Use Claude CLI mode" checkbox**: derives credentials automatically from existing OAuth2 tokens already stored in AISBF, converting them to the `claudeAiOauth` schema expected by the CLI
- **Explicit upload**: users can upload their own `~/.claude/.credentials.json` file to use a specific account (takes priority over OAuth2-derived credentials)
- Admin (config-file user): CLI credentials file path stored in `providers.json` under `claude_config.cli_credentials_file`
- Database users (all roles): CLI credentials stored in `user_oauth2_credentials` with `auth_type='claude_cli_credentials'`
##### Credentials file schema
The CLI expects a `.credentials.json` with this structure:
```json
{
"claudeAiOauth": {
"accessToken": "sk-ant-oat01-...",
"refreshToken": "sk-ant-ort01-...",
"expiresAt": 1776940481301,
"scopes": ["user:inference", "user:file_upload", "user:profile", "user:mcp_servers", "user:sessions:claude_code"],
"subscriptionType": "pro",
"rateLimitTier": "default_claude_ai"
}
}
```
When "Use Claude CLI mode" is checked without an explicit file upload, AISBF converts the stored OAuth2 tokens to this format automatically (`expires_at` seconds → `expiresAt` milliseconds, `scope` string → `scopes` list).
### Ollama
- Uses direct HTTP API
......
......@@ -19,7 +19,8 @@ Also available via TOR for privacy-first access:
## Key Features
- **Multi-Provider Support**: Unified interface for Google, OpenAI, Anthropic, Claude Code (OAuth2), Ollama, Kiro, Kilocode, Codex, and Qwen
- **Multi-Provider Support**: Unified interface for Google, OpenAI, Anthropic, Claude Code (OAuth2 or CLI), Ollama, Kiro, Kilocode, Codex, and Qwen
- **Claude CLI Mode**: When the `claude` binary is in PATH, requests are proxied through the official Anthropic CLI (`claude -p`) — uses each user's own account, fully permitted by Claude's terms of service
- **Unified Wallet System**: Fiat wallet with crypto/PayPal/Stripe top-ups and auto top-up for subscription renewals
- **Intelligent Routing**: Weighted load balancing and AI-assisted model selection
- **Streaming Support**: Full support for streaming responses from all providers
......
......@@ -54,7 +54,7 @@ from .auth.qwen import QwenOAuth2
from .handlers import RequestHandler, RotationHandler, AutoselectHandler
from .utils import count_messages_tokens, split_messages_into_chunks, get_max_request_tokens_for_model
__version__ = "0.99.56"
__version__ = "0.99.57"
__all__ = [
# Config
"config",
......
"""
Copyleft (C) 2026 Stefy Lanza <stefy@nexlab.net>
AISBF - AI Service Broker Framework || AI Should Be Free
Claude CLI mode detection module.
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
import shutil
from typing import Optional
CLAUDE_CLI_MODE: bool = False
CLAUDE_CLI_PATH: Optional[str] = None
def detect_claude_cli() -> bool:
global CLAUDE_CLI_MODE, CLAUDE_CLI_PATH
path = shutil.which('claude')
if path:
CLAUDE_CLI_MODE = True
CLAUDE_CLI_PATH = path
return True
return False
......@@ -24,13 +24,108 @@ import httpx
import asyncio
import time
import random
from typing import Dict, List, Optional, Union, Any
import os
import json
import shutil
import tempfile
import logging as _logging
from typing import Dict, List, Optional, Union, Any, Tuple
from anthropic import Anthropic
from ..models import Model
from ..config import config
from .base import BaseProviderHandler, AnthropicFormatConverter, AISBF_DEBUG
class ClaudeCliSessionManager:
"""
Manages per-user temporary config directories for the claude CLI subprocess.
Each (user_id, provider_id) pair gets its own temp dir containing
settings.json and .credentials.json. The dir is reused across requests
and cleaned up after 10 minutes of idle time.
"""
_sessions: Dict[Tuple, Dict] = {}
_cleanup_task: Optional[asyncio.Task] = None
_lock: Optional[asyncio.Lock] = None
_IDLE_TIMEOUT = 600 # 10 minutes
@classmethod
def _get_lock(cls) -> asyncio.Lock:
if cls._lock is None:
cls._lock = asyncio.Lock()
return cls._lock
@classmethod
async def get_config_dir(cls, user_id, provider_id: str, cli_credentials: dict) -> str:
"""Return (creating if needed) the temp config dir for this user/provider."""
logger = _logging.getLogger(__name__)
key = (user_id, provider_id)
async with cls._get_lock():
if key in cls._sessions:
session = cls._sessions[key]
if os.path.exists(session['config_dir']):
session['last_used'] = time.time()
logger.debug(f"ClaudeCliMode: reusing session dir for {key}")
return session['config_dir']
del cls._sessions[key]
temp_dir = tempfile.mkdtemp(prefix='aisbf_claude_cli_')
# settings.json – prevents interactive onboarding hang
with open(os.path.join(temp_dir, 'settings.json'), 'w') as fh:
json.dump({
'hasCompletedOnboarding': True,
'telemetry': 'off',
'theme': 'dark',
}, fh, indent=2)
# .credentials.json – the user's Claude CLI OAuth tokens
with open(os.path.join(temp_dir, '.credentials.json'), 'w') as fh:
json.dump(cli_credentials, fh, indent=2)
cls._sessions[key] = {'config_dir': temp_dir, 'last_used': time.time()}
logger.info(f"ClaudeCliMode: created session dir {temp_dir} for {key}")
cls._ensure_cleanup_task()
return temp_dir
@classmethod
def _ensure_cleanup_task(cls):
try:
loop = asyncio.get_event_loop()
if cls._cleanup_task is None or cls._cleanup_task.done():
cls._cleanup_task = loop.create_task(cls._cleanup_loop())
except RuntimeError:
pass
@classmethod
async def _cleanup_loop(cls):
logger = _logging.getLogger(__name__)
while True:
await asyncio.sleep(60)
async with cls._get_lock():
if not cls._sessions:
cls._cleanup_task = None
return
now = time.time()
expired = [k for k, s in cls._sessions.items()
if now - s['last_used'] > cls._IDLE_TIMEOUT]
for key in expired:
session = cls._sessions.pop(key)
try:
shutil.rmtree(session['config_dir'], ignore_errors=True)
logger.info(
f"ClaudeCliMode: cleaned up idle session "
f"{session['config_dir']} for {key}"
)
except Exception as exc:
logger.warning(
f"ClaudeCliMode: cleanup error for "
f"{session['config_dir']}: {exc}"
)
class ClaudeProviderHandler(BaseProviderHandler):
"""
Handler for Claude Code OAuth2 integration using Anthropic SDK.
......@@ -155,7 +250,294 @@ class ClaudeProviderHandler(BaseProviderHandler):
# For regular users, NO file fallback - return empty auth instance
logging.getLogger(__name__).info(f"ClaudeProviderHandler: No database credentials found for user {self.user_id}, returning unauthenticated instance")
return ClaudeAuth(credentials_file=credentials_file, skip_initial_load=True)
# ------------------------------------------------------------------ #
# Claude CLI mode helpers #
# ------------------------------------------------------------------ #
@staticmethod
def _oauth_tokens_to_cli_credentials(tokens: dict) -> dict:
"""
Convert AISBF OAuth2 token dict to the Claude CLI .credentials.json schema:
AISBF stores: access_token, refresh_token, expires_at (seconds float), scope
CLI expects: claudeAiOauth.accessToken, .refreshToken, .expiresAt (ms int),
.scopes (list), .subscriptionType, .rateLimitTier
"""
default_scopes = [
'user:file_upload',
'user:inference',
'user:mcp_servers',
'user:profile',
'user:sessions:claude_code',
]
raw_scope = tokens.get('scope', '')
scopes = raw_scope.split() if raw_scope.strip() else default_scopes
expires_at_sec = tokens.get('expires_at', 0)
expires_at_ms = int(expires_at_sec * 1000) if expires_at_sec else 0
return {
'claudeAiOauth': {
'accessToken': tokens.get('access_token', ''),
'refreshToken': tokens.get('refresh_token', ''),
'expiresAt': expires_at_ms,
'scopes': scopes,
'subscriptionType': tokens.get('subscription_type', 'pro'),
'rateLimitTier': tokens.get('rate_limit_tier', 'default_claude_ai'),
}
}
def _get_cli_credentials(self) -> Optional[dict]:
"""
Return the Claude CLI .credentials.json content for this user/provider,
or None if CLI credentials are not configured.
Priority order:
1. Explicit CLI credentials file (admin) or uploaded CLI credentials (DB user)
2. If claude_config.use_cli_mode is true, derive from existing OAuth2 tokens
"""
logger = _logging.getLogger(__name__)
if isinstance(self.provider_config, dict):
claude_cfg = self.provider_config.get('claude_config', {}) or {}
else:
claude_cfg = getattr(self.provider_config, 'claude_config', {}) or {}
use_cli_mode = bool(claude_cfg.get('use_cli_mode')) if isinstance(claude_cfg, dict) else False
if self.user_id is None:
# ── Config admin ──────────────────────────────────────────────
cli_file = claude_cfg.get('cli_credentials_file') if isinstance(claude_cfg, dict) else None
if cli_file:
expanded = os.path.expanduser(cli_file)
if not os.path.exists(expanded):
logger.warning(f"ClaudeCliMode: CLI credentials file not found: {expanded}")
else:
try:
with open(expanded) as fh:
return json.load(fh)
except Exception as exc:
logger.warning(f"ClaudeCliMode: failed to read CLI credentials file: {exc}")
# Fall back: derive from existing OAuth2 tokens when use_cli_mode is set
if use_cli_mode and self.auth and self.auth.tokens:
logger.info("ClaudeCliMode: building CLI credentials from existing OAuth2 tokens (admin)")
return self._oauth_tokens_to_cli_credentials(self.auth.tokens)
return None
else:
# ── DB user ───────────────────────────────────────────────────
try:
from ..database import DatabaseRegistry
db = DatabaseRegistry.get_config_database()
if db:
# 1. Check for explicit uploaded CLI credentials
row = db.get_user_oauth2_credentials(
user_id=self.user_id,
provider_id=self.provider_id,
auth_type='claude_cli_credentials',
)
if row and row.get('credentials'):
return row['credentials'].get('credentials')
# 2. Derive from existing OAuth2 tokens when use_cli_mode is set
if use_cli_mode:
oauth_row = db.get_user_oauth2_credentials(
user_id=self.user_id,
provider_id=self.provider_id,
auth_type='claude_oauth2',
)
if oauth_row and oauth_row.get('credentials'):
tokens = oauth_row['credentials'].get('tokens', {})
if tokens:
logger.info(
f"ClaudeCliMode: building CLI credentials from "
f"OAuth2 tokens for user {self.user_id}"
)
return self._oauth_tokens_to_cli_credentials(tokens)
except Exception as exc:
logger.warning(f"ClaudeCliMode: failed to load credentials: {exc}")
return None
def _messages_to_cli_prompt(self, messages: List[Dict]) -> str:
"""
Convert an OpenAI-style messages list to a flat text prompt for the
claude CLI. System messages are included as a prefix; no Anthropic
system-prompt injection is needed in CLI mode.
"""
system_parts: List[str] = []
turn_parts: List[str] = []
for msg in messages:
role = msg.get('role', '')
content = msg.get('content', '')
# Normalise content to str
if isinstance(content, list):
text_fragments = []
for block in content:
if isinstance(block, dict) and block.get('type') == 'text':
text_fragments.append(block.get('text', ''))
elif isinstance(block, str):
text_fragments.append(block)
content = '\n'.join(text_fragments)
elif not isinstance(content, str):
content = str(content)
if role == 'system':
system_parts.append(content.strip())
elif role == 'user':
turn_parts.append(f'Human: {content}')
elif role == 'assistant':
turn_parts.append(f'Assistant: {content}')
parts: List[str] = []
if system_parts:
parts.append('[System Instructions: ' + '\n'.join(system_parts) + ']')
parts.extend(turn_parts)
return '\n\n'.join(parts)
async def _handle_cli_streaming_request(self, prompt: str, model: str, config_dir: str):
"""
Spawn a claude CLI subprocess, stream its JSON output, and yield
OpenAI-compatible SSE chunks. Multiple parallel calls each get their
own subprocess; the config_dir is shared (read-only at runtime).
"""
logger = _logging.getLogger(__name__)
clean_model = model.split('/')[-1] if '/' in model else model
env = os.environ.copy()
env['CLAUDE_CONFIG_DIR'] = config_dir
env['CLAUDE_CODE_USE_KEYCHAIN'] = 'false'
cmd = [
'claude', '-p',
'--input-format', 'stream-json',
'--output-format', 'stream-json',
'--tools', '',
'--dangerously-skip-permissions',
'--no-session-persistence',
'--verbose',
]
if clean_model:
cmd += ['--model', clean_model]
logger.info(f"ClaudeCliMode: launching subprocess model={clean_model} dir={config_dir}")
process = await asyncio.create_subprocess_exec(
*cmd,
env=env,
stdin=asyncio.subprocess.PIPE,
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
)
# Send the prompt as a stream-json user_message then close stdin
input_msg = json.dumps({
'type': 'user_message',
'content': [{'type': 'text', 'text': prompt}],
}) + '\n'
process.stdin.write(input_msg.encode())
await process.stdin.drain()
process.stdin.close()
completion_id = f'chatcmpl-cli-{int(time.time())}'
created_time = int(time.time())
first_chunk = True
try:
while True:
try:
raw = await asyncio.wait_for(process.stdout.readline(), timeout=120.0)
except asyncio.TimeoutError:
logger.error("ClaudeCliMode: subprocess read timeout (120 s)")
break
if not raw:
break
line_str = raw.decode('utf-8', errors='replace').strip()
if not line_str:
continue
try:
data = json.loads(line_str)
except json.JSONDecodeError:
continue
event_type = data.get('type')
if event_type == 'content_block_delta':
delta = data.get('delta', {})
if delta.get('type') == 'text_delta':
text = delta.get('text', '')
if not text:
continue
if first_chunk:
yield f'data: {json.dumps({"id": completion_id, "object": "chat.completion.chunk", "created": created_time, "model": f"{self.provider_id}/{clean_model}", "choices": [{"index": 0, "delta": {"role": "assistant", "content": ""}, "finish_reason": None}]})}\n\n'
first_chunk = False
yield f'data: {json.dumps({"id": completion_id, "object": "chat.completion.chunk", "created": created_time, "model": f"{self.provider_id}/{clean_model}", "choices": [{"index": 0, "delta": {"content": text}, "finish_reason": None}]})}\n\n'
elif event_type in ('message_stop', 'result'):
logger.debug(f"ClaudeCliMode: received {event_type}, closing stream")
break
except Exception as exc:
logger.error(f"ClaudeCliMode: streaming error: {exc}", exc_info=True)
try:
stderr_bytes = await asyncio.wait_for(process.stderr.read(), timeout=2.0)
if stderr_bytes:
logger.error(f"ClaudeCliMode: stderr: {stderr_bytes.decode('utf-8', errors='replace')[:500]}")
except Exception:
pass
finally:
try:
process.terminate()
await asyncio.wait_for(process.wait(), timeout=5.0)
except Exception:
try:
process.kill()
except Exception:
pass
# Final stop + DONE
yield f'data: {json.dumps({"id": completion_id, "object": "chat.completion.chunk", "created": created_time, "model": f"{self.provider_id}/{clean_model}", "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}]})}\n\n'
yield 'data: [DONE]\n\n'
async def _handle_cli_request(self, prompt: str, model: str, config_dir: str) -> dict:
"""Non-streaming CLI request – collects the full response text."""
accumulated: List[str] = []
async for chunk_str in self._handle_cli_streaming_request(prompt, model, config_dir):
if chunk_str.startswith('data: ') and '[DONE]' not in chunk_str:
try:
data = json.loads(chunk_str[6:])
choices = data.get('choices', [])
if choices:
text = choices[0].get('delta', {}).get('content', '')
if text:
accumulated.append(text)
except json.JSONDecodeError:
pass
clean_model = model.split('/')[-1] if '/' in model else model
full_text = ''.join(accumulated)
return {
'id': f'chatcmpl-cli-{int(time.time())}',
'object': 'chat.completion',
'created': int(time.time()),
'model': f'{self.provider_id}/{clean_model}',
'choices': [{
'index': 0,
'message': {'role': 'assistant', 'content': full_text},
'finish_reason': 'stop',
}],
'usage': {'prompt_tokens': 0, 'completion_tokens': 0, 'total_tokens': 0},
}
def _init_session_identifiers(self):
"""Initialize persistent session identifiers (device_id, account_uuid, session_id)."""
import uuid
......@@ -832,13 +1214,33 @@ class ClaudeProviderHandler(BaseProviderHandler):
async def _handle_request_with_model(self, model: str, messages: List[Dict], max_tokens: Optional[int] = None,
temperature: Optional[float] = 1.0, stream: Optional[bool] = False,
tools: Optional[List[Dict]] = None, tool_choice: Optional[Union[str, Dict]] = None) -> Union[Dict, object]:
"""Handle request with a specific model using direct HTTP requests."""
"""Handle request with a specific model.
When the claude CLI is detected at startup and the user has CLI
credentials configured, the request is proxied through a subprocess
instead of calling the Anthropic HTTP API directly.
"""
import logging
import json
logger = logging.getLogger(__name__)
# ── Claude CLI mode ──────────────────────────────────────────────
import aisbf.cli_mode as cli_mode_mod
if cli_mode_mod.CLAUDE_CLI_MODE:
cli_creds = self._get_cli_credentials()
if cli_creds is not None:
logger.info(f"ClaudeProviderHandler: using CLI subprocess mode for model {model}")
prompt = self._messages_to_cli_prompt(messages)
config_dir = await ClaudeCliSessionManager.get_config_dir(
self.user_id, self.provider_id, cli_creds
)
if stream:
return self._handle_cli_streaming_request(prompt, model, config_dir)
else:
return await self._handle_cli_request(prompt, model, config_dir)
# ── Fall through to HTTP API mode ────────────────────────────────
logger.info(f"ClaudeProviderHandler: Handling request for model {model} (Direct HTTP mode)")
if AISBF_DEBUG:
logger.info(f"ClaudeProviderHandler: Messages: {messages}")
else:
......
......@@ -663,6 +663,7 @@ autoselect_handler = None
server_config = None
config = None
_initialized = False
_claude_cli_mode = False
# Cache for user-specific handlers to avoid recreating them
_user_handlers_cache = {}
......@@ -712,20 +713,30 @@ _background_tasks: set = set()
def initialize_app(custom_config_dir=None):
"""Initialize app globals. Called by startup event or main()."""
global config, request_handler, rotation_handler, autoselect_handler, server_config, _initialized
global config, request_handler, rotation_handler, autoselect_handler, server_config, _initialized, _claude_cli_mode
if _initialized:
return
# Set custom config directory if provided
if custom_config_dir:
set_config_dir(custom_config_dir)
logger.info(f"Using custom config directory: {custom_config_dir}")
# Detect claude CLI in PATH and enable CLI proxy mode if found
import shutil as _shutil
from aisbf.cli_mode import detect_claude_cli
if detect_claude_cli():
_claude_cli_mode = True
import aisbf.cli_mode as _cli_mode_mod
logger.info(f"Claude CLI detected at {_cli_mode_mod.CLAUDE_CLI_PATH} – CLI proxy mode enabled")
else:
logger.info("Claude CLI not found in PATH – using HTTP API mode")
# Import config
from aisbf.config import config as cfg
from aisbf.handlers import RequestHandler, RotationHandler, AutoselectHandler
config = cfg
request_handler = RequestHandler()
rotation_handler = RotationHandler()
......@@ -4387,6 +4398,7 @@ async def dashboard_providers(request: Request):
"session": request.session,
"__version__": __version__,
"providers_json": json.dumps(providers_data),
"claude_cli_mode": _claude_cli_mode,
"success": "Configuration saved successfully! Restart server for changes to take effect." if success else None
}
)
......@@ -4401,6 +4413,7 @@ async def dashboard_providers(request: Request):
"__version__": __version__,
"user_providers_json": json.dumps(providers_data),
"user_id": current_user_id,
"claude_cli_mode": _claude_cli_mode,
"success": "Configuration saved successfully!" if success else None
}
)
......@@ -4607,12 +4620,13 @@ async def dashboard_providers_save(request: Request, config: str = Form(...)):
"session": request.session,
"__version__": __version__,
"providers_json": json.dumps(providers_data),
"claude_cli_mode": _claude_cli_mode,
"success": success_msg
}
)
else:
success_msg = "Configuration saved successfully!"
return templates.TemplateResponse(
request=request,
name="dashboard/user_providers.html",
......@@ -4622,6 +4636,7 @@ async def dashboard_providers_save(request: Request, config: str = Form(...)):
"__version__": __version__,
"user_providers_json": json.dumps(providers_data),
"user_id": current_user_id,
"claude_cli_mode": _claude_cli_mode,
"success": success_msg
}
)
......@@ -4629,20 +4644,20 @@ async def dashboard_providers_save(request: Request, config: str = Form(...)):
# Reload current config on error
current_user_id = request.session.get('user_id')
is_config_admin = current_user_id is None
if is_config_admin:
config_path = Path.home() / '.aisbf' / 'providers.json'
if not config_path.exists():
config_path = Path(__file__).parent / 'config' / 'providers.json'
with open(config_path) as f:
full_config = json.load(f)
# Extract providers
if 'providers' in full_config and isinstance(full_config['providers'], dict):
providers_data = full_config['providers']
else:
providers_data = {k: v for k, v in full_config.items() if k != 'condensation'}
return templates.TemplateResponse(
request=request,
name="dashboard/providers.html",
......@@ -4651,13 +4666,14 @@ async def dashboard_providers_save(request: Request, config: str = Form(...)):
"session": request.session,
"__version__": __version__,
"providers_json": json.dumps(providers_data),
"claude_cli_mode": _claude_cli_mode,
"error": f"Invalid JSON: {str(e)}"
}
)
else:
db = DatabaseRegistry.get_config_database()
user_providers = db.get_user_providers(current_user_id)
return templates.TemplateResponse(
request=request,
name="dashboard/user_providers.html",
......@@ -4667,6 +4683,7 @@ async def dashboard_providers_save(request: Request, config: str = Form(...)):
"__version__": __version__,
"user_providers_json": json.dumps(user_providers),
"user_id": current_user_id,
"claude_cli_mode": _claude_cli_mode,
"error": f"Invalid JSON: {str(e)}"
}
)
......@@ -6321,7 +6338,7 @@ async def dashboard_provider_upload(
try:
# Validate file type
allowed_types = ['credentials', 'database', 'config', 'kiro_credentials', 'claude_credentials', 'sqlite_db', 'creds_file']
allowed_types = ['credentials', 'database', 'config', 'kiro_credentials', 'claude_credentials', 'sqlite_db', 'creds_file', 'cli_credentials']
if file_type not in allowed_types:
return JSONResponse(
status_code=400,
......@@ -6486,7 +6503,7 @@ async def dashboard_provider_upload_form(
try:
# Validate file type
allowed_types = ['credentials', 'database', 'config', 'kiro_credentials', 'claude_credentials', 'sqlite_db', 'creds_file']
allowed_types = ['credentials', 'database', 'config', 'kiro_credentials', 'claude_credentials', 'sqlite_db', 'creds_file', 'cli_credentials']
if file_type not in allowed_types:
return JSONResponse(
status_code=400,
......@@ -6582,7 +6599,7 @@ async def dashboard_provider_upload_chunk(
try:
# Validate file type
allowed_types = ['credentials', 'database', 'config', 'kiro_credentials', 'claude_credentials', 'sqlite_db', 'creds_file']
allowed_types = ['credentials', 'database', 'config', 'kiro_credentials', 'claude_credentials', 'sqlite_db', 'creds_file', 'cli_credentials']
if file_type not in allowed_types:
return JSONResponse(
status_code=400,
......@@ -6636,16 +6653,38 @@ async def dashboard_provider_upload_chunk(
# Save metadata to database if not admin
if not is_config_admin:
db = DatabaseRegistry.get_config_database()
db.save_user_auth_file(
user_id=current_user_id,
provider_id=provider_key,
file_type=file_type,
original_filename=file_name,
stored_filename=stored_filename,
file_path=str(file_path),
file_size=total_size,
mime_type=file.content_type
)
# CLI credentials for claude providers go directly into
# user_oauth2_credentials so the provider handler can read them
# without touching the filesystem at request time.
if file_type == 'cli_credentials':
try:
with open(file_path, 'r') as fh:
cli_creds_content = json.load(fh)
db.save_user_oauth2_credentials(
user_id=current_user_id,
provider_id=provider_key,
auth_type='claude_cli_credentials',
credentials={'credentials': cli_creds_content},
)
file_path.unlink(missing_ok=True)
logger.info(
f"Stored CLI credentials for user {current_user_id} "
f"provider {provider_key} in user_oauth2_credentials"
)
except Exception as exc:
logger.error(f"Failed to save CLI credentials to DB: {exc}")
else:
db.save_user_auth_file(
user_id=current_user_id,
provider_id=provider_key,
file_type=file_type,
original_filename=file_name,
stored_filename=stored_filename,
file_path=str(file_path),
file_size=total_size,
mime_type=file.content_type
)
else:
# Config admin: update providers.json with full path
try:
......@@ -6704,7 +6743,14 @@ async def dashboard_provider_upload_chunk(
providers[provider_key]['codex_config'] = {}
providers[provider_key]['codex_config']['credentials_file'] = relative_path
logger.info(f"Updated providers.json: {provider_key}.codex_config.credentials_file = {relative_path}")
# For Claude CLI credentials – store path in claude_config.cli_credentials_file
elif provider_type == 'claude' and file_type == 'cli_credentials':
if 'claude_config' not in providers[provider_key]:
providers[provider_key]['claude_config'] = {}
providers[provider_key]['claude_config']['cli_credentials_file'] = relative_path
logger.info(f"Updated providers.json: {provider_key}.claude_config.cli_credentials_file = {relative_path}")
# Fallback: update top-level field
else:
providers[provider_key][file_type] = relative_path
......
......@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "aisbf"
version = "0.99.56"
version = "0.99.57"
description = "AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations"
readme = "README.md"
license = "GPL-3.0-or-later"
......
......@@ -49,7 +49,7 @@ class InstallCommand(_install):
setup(
name="aisbf",
version="0.99.56",
version="0.99.57",
author="AISBF Contributors",
author_email="stefy@nexlab.net",
description="AISBF - AI Service Broker Framework || AI Should Be Free - A modular proxy server for managing multiple AI provider integrations",
......
......@@ -64,11 +64,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div id="new-provider-claude-warning" style="background: #ffdd57; border: 2px solid #ff6b35; border-radius: 8px; padding: 20px; margin-bottom: 15px; display: none;">
<h4 style="margin: 0 0 10px 0; color: #d63031;">⚠️ Important Notice for Claude Provider</h4>
<p style="margin: 0; color: #2d3436;">
Claude.ai policies state that unofficial clients are not allowed. By using AISBF as a client, you acknowledge that:
Claude.ai policies state that unofficial clients are not allowed. By using AISBF in <strong>HTTP API / OAuth2 mode</strong>, you acknowledge that:
<br><br>• While we do our best to mimic the claude-code CLI, using an unofficial client can lead to account suspension or cancellation
<br>• Claude.ai detects prompt patterns to identify unofficial client usage, so it may not work with complex agents and prompts
<br>• You use this software at your own risk and responsibility
<br>• We are not affiliated with Anthropic and cannot guarantee compatibility or continued functionality
<br><br><strong>CLI mode is different:</strong> when "Use Claude CLI mode" is enabled, AISBF proxies requests through the official <code>claude</code> binary using <code>claude -p</code>, which is the intended programmatic use of the official Anthropic CLI and is permitted by Claude's terms of service.
</p>
</div>
......@@ -134,6 +135,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{% block extra_js %}
<script>
const CLAUDE_CLI_MODE = {{ 'true' if claude_cli_mode else 'false' }};
let providersData = {{ providers_json | replace("</script>", "<\\/script>") | safe }};
let expandedProviders = new Set();
......@@ -227,6 +229,41 @@ async function uploadClaudeFile(providerKey, file) {
await uploadFileChunked(providerKey, 'credentials_file', file, 'claude_config');
}
// Upload handler for Claude CLI credentials (.credentials.json from ~/.claude/)
async function uploadClaudeCliFile(providerKey, file) {
if (!file) return;
if (!expandedProviders.has(providerKey)) toggleProvider(providerKey);
const statusEl = document.getElementById(`claude-cli-upload-status-${providerKey}`);
if (!statusEl) return;
statusEl.innerHTML = '<div style="color: #4a9eff;">Uploading CLI credentials: 0%</div>';
try {
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
for (let chunkNumber = 1; chunkNumber <= totalChunks; chunkNumber++) {
const start = (chunkNumber - 1) * CHUNK_SIZE;
const chunk = file.slice(start, Math.min(start + CHUNK_SIZE, file.size));
const progress = Math.round((chunkNumber / totalChunks) * 100);
statusEl.innerHTML = `<div style="color: #4a9eff;">Uploading CLI credentials: ${progress}%</div>`;
const formData = new FormData();
formData.append('provider_key', providerKey);
formData.append('file_type', 'cli_credentials');
formData.append('file_name', file.name);
formData.append('total_size', file.size);
formData.append('chunk_number', chunkNumber);
formData.append('total_chunks', totalChunks);
formData.append('file', chunk);
const response = await fetch('/dashboard/providers/upload-auth-file/chunk', { method: 'POST', body: formData });
const result = await response.json();
if (!result.success) throw new Error(result.error || 'Upload failed');
if (result.complete) {
statusEl.innerHTML = '<div style="color: #4ade80;">CLI credentials saved to server!</div>';
return;
}
}
} catch (e) {
statusEl.innerHTML = `<div style="color: #f87171;">Upload failed: ${e.message}</div>`;
}
}
// Upload handler for Qwen credential files
async function uploadQwenFile(providerKey, file) {
await uploadFileChunked(providerKey, 'credentials_file', file, 'qwen_config');
......@@ -505,21 +542,60 @@ function renderProviderDetails(key) {
<h5 style="margin: 20px 0 10px 0; color: #8ec8ff;">Or Upload Credentials File</h5>
<div class="form-group">
<label>Upload Credentials File</label>
<label>Upload OAuth2 Credentials File</label>
<input type="file" id="claude-creds-file-${key}" accept=".json" onchange="uploadClaudeFile('${key}', this.files[0])">
<small style="color: #a0a0a0; display: block; margin-top: 5px;">Upload Claude OAuth2 credentials JSON file</small>
</div>
<div id="claude-upload-status-${key}" style="margin-top: 10px;"></div>
${CLAUDE_CLI_MODE ? `
<div style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #1e3a5f;">
<h5 style="margin: 0 0 8px 0; color: #4ade80;">Claude CLI Mode Active</h5>
<small style="color: #4ade80; display: block; margin-bottom: 14px;">
The claude CLI was detected at startup. When enabled, requests are piped
through the local claude binary instead of the HTTP API.
</small>
<div class="form-group" style="margin-bottom: 16px;">
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="checkbox"
id="claude-use-cli-mode-${key}"
${claudeConfig.use_cli_mode ? 'checked' : ''}
onchange="updateClaudeConfig('${key}', 'use_cli_mode', this.checked)"
style="width: 18px; height: 18px; cursor: pointer; accent-color: #4ade80;">
<span style="color: #e0e0e0; font-weight: 500;">Use Claude CLI mode</span>
</label>
<small style="color: #a0a0a0; display: block; margin-top: 6px; padding-left: 28px;">
When checked and you have already authenticated via OAuth2 above,
your existing tokens are used automatically — no separate file upload needed.
You can also upload an explicit <code style="background:#0f2840;padding:1px 4px;border-radius:3px;">.credentials.json</code>
below to override.
</small>
</div>
<div class="form-group">
<label>Override: Upload CLI Credentials File</label>
<input type="file" id="claude-cli-creds-file-${key}" accept=".json" onchange="uploadClaudeCliFile('${key}', this.files[0])">
<small style="color: #a0a0a0; display: block; margin-top: 5px;">
Optional — upload a <code style="background:#0f2840;padding:1px 4px;border-radius:3px;">~/.claude/.credentials.json</code>
to use a specific account. Path saved in
<code style="background:#0f2840;padding:1px 4px;border-radius:3px;">claude_config.cli_credentials_file</code>.
</small>
</div>
<div id="claude-cli-upload-status-${key}" style="margin-top: 10px;"></div>
</div>
` : ''}
</div>
<div style="background: #ffdd57; border: 2px solid #ff6b35; border-radius: 8px; padding: 20px; margin-bottom: 15px;">
<h4 style="margin: 0 0 10px 0; color: #d63031;">⚠️ Important Notice</h4>
<p style="margin: 0; color: #2d3436;">
Claude.ai policies state that unofficial clients are not allowed. By using AISBF as a client, you acknowledge that:
Claude.ai policies state that unofficial clients are not allowed. By using AISBF in <strong>HTTP API / OAuth2 mode</strong>, you acknowledge that:
<br><br>• While we do our best to mimic the claude-code CLI, using an unofficial client can lead to account suspension or cancellation
<br>• Claude.ai detects prompt patterns to identify unofficial client usage, so it may not work with complex agents and prompts
<br>• You use this software at your own risk and responsibility
<br>• We are not affiliated with Anthropic and cannot guarantee compatibility or continued functionality
<br><br><strong>CLI mode is different:</strong> when "Use Claude CLI mode" is enabled, AISBF proxies requests through the official <code style="background:#e8e0b0;padding:1px 4px;border-radius:3px;">claude</code> binary using <code style="background:#e8e0b0;padding:1px 4px;border-radius:3px;">claude -p</code>, which is the intended programmatic use of the official Anthropic CLI and is permitted by Claude's terms of service.
</p>
</div>
`;
......
......@@ -64,11 +64,12 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
<div id="new-provider-claude-warning" style="background: #ffdd57; border: 2px solid #ff6b35; border-radius: 8px; padding: 20px; margin-bottom: 15px; display: none;">
<h4 style="margin: 0 0 10px 0; color: #d63031;">⚠️ Important Notice for Claude Provider</h4>
<p style="margin: 0; color: #2d3436;">
Claude.ai policies state that unofficial clients are not allowed. By using AISBF as a client, you acknowledge that:
Claude.ai policies state that unofficial clients are not allowed. By using AISBF as a client in <strong>HTTP API / OAuth2 mode</strong>, you acknowledge that:
<br><br>• While we do our best to mimic the claude-code CLI, using an unofficial client can lead to account suspension or cancellation
<br>• Claude.ai detects prompt patterns to identify unofficial client usage, so it may not work with complex agents and prompts
<br>• You use this software at your own risk and responsibility
<br>• We are not affiliated with Anthropic and cannot guarantee compatibility or continued functionality
<br><br><strong>CLI mode is different:</strong> when "Use Claude CLI mode" is enabled, AISBF proxies requests through the official <code>claude</code> binary using <code>claude -p</code>, which is the intended programmatic use of the official Anthropic CLI and is permitted by Claude's terms of service.
</p>
</div>
......@@ -134,6 +135,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
{% block extra_js %}
<script>
const CLAUDE_CLI_MODE = {{ 'true' if claude_cli_mode else 'false' }};
// Global cache settings cache (pun intended)
let cacheSettings = [];
......@@ -353,6 +356,41 @@ async function uploadClaudeFile(providerKey, file) {
await uploadFileChunked(providerKey, 'credentials_file', file, 'claude_config');
}
// Upload handler for Claude CLI credentials file (.credentials.json from ~/.claude/)
async function uploadClaudeCliFile(providerKey, file) {
if (!file) return;
if (!expandedProviders.has(providerKey)) toggleProvider(providerKey);
const statusEl = document.getElementById(`claude-cli-upload-status-${providerKey}`);
if (!statusEl) return;
statusEl.innerHTML = '<div style="color: #4a9eff;">Uploading CLI credentials: 0%</div>';
try {
const totalChunks = Math.ceil(file.size / CHUNK_SIZE);
for (let chunkNumber = 1; chunkNumber <= totalChunks; chunkNumber++) {
const start = (chunkNumber - 1) * CHUNK_SIZE;
const chunk = file.slice(start, Math.min(start + CHUNK_SIZE, file.size));
const progress = Math.round((chunkNumber / totalChunks) * 100);
statusEl.innerHTML = `<div style="color: #4a9eff;">Uploading CLI credentials: ${progress}%</div>`;
const formData = new FormData();
formData.append('provider_key', providerKey);
formData.append('file_type', 'cli_credentials');
formData.append('file_name', file.name);
formData.append('total_size', file.size);
formData.append('chunk_number', chunkNumber);
formData.append('total_chunks', totalChunks);
formData.append('file', chunk);
const response = await fetch('/dashboard/providers/upload-auth-file/chunk', { method: 'POST', body: formData });
const result = await response.json();
if (!result.success) throw new Error(result.error || 'Upload failed');
if (result.complete) {
statusEl.innerHTML = '<div style="color: #4ade80;">CLI credentials uploaded and saved!</div>';
return;
}
}
} catch (e) {
statusEl.innerHTML = `<div style="color: #f87171;">Upload failed: ${e.message}</div>`;
}
}
// Upload handler for Qwen credential files
async function uploadQwenFile(providerKey, file) {
await uploadFileChunked(providerKey, 'credentials_file', file, 'qwen_config');
......@@ -596,23 +634,61 @@ function renderProviderDetails(key) {
<!-- Auth status will be displayed here -->
</div>
<h5 style="margin: 20px 0 10px 0; color: #8ec8ff;">Or Upload Credentials File</h5>
<h5 style="margin: 20px 0 10px 0; color: #8ec8ff;">Or Upload OAuth2 Credentials File</h5>
<div class="form-group">
<label>Upload Credentials File</label>
<label>Upload OAuth2 Credentials File</label>
<input type="file" id="claude-creds-file-${key}" accept=".json" onchange="uploadClaudeFile('${key}', this.files[0])">
<small style="color: #a0a0a0; display: block; margin-top: 5px;">Upload Claude OAuth2 credentials JSON file</small>
</div>
<div id="claude-upload-status-${key}" style="margin-top: 10px;"></div>
${CLAUDE_CLI_MODE ? `
<div style="margin-top: 20px; padding-top: 15px; border-top: 1px solid #1e3a5f;">
<h5 style="margin: 0 0 8px 0; color: #4ade80;">Claude CLI Mode Active</h5>
<small style="color: #4ade80; display: block; margin-bottom: 14px;">
The claude CLI was detected at startup. When enabled, requests are piped
through the local claude binary instead of the HTTP API.
</small>
<div class="form-group" style="margin-bottom: 16px;">
<label style="display: flex; align-items: center; gap: 10px; cursor: pointer;">
<input type="checkbox"
id="claude-use-cli-mode-${key}"
${claudeConfig.use_cli_mode ? 'checked' : ''}
onchange="updateClaudeConfig('${key}', 'use_cli_mode', this.checked)"
style="width: 18px; height: 18px; cursor: pointer; accent-color: #4ade80;">
<span style="color: #e0e0e0; font-weight: 500;">Use Claude CLI mode</span>
</label>
<small style="color: #a0a0a0; display: block; margin-top: 6px; padding-left: 28px;">
When checked and you have already authenticated via OAuth2 above,
your existing tokens will be used automatically — no separate file upload needed.
You can also upload an explicit <code style="background:#0f2840;padding:1px 4px;border-radius:3px;">.credentials.json</code>
below to override.
</small>
</div>
<div class="form-group">
<label>Override: Upload CLI Credentials File</label>
<input type="file" id="claude-cli-creds-file-${key}" accept=".json" onchange="uploadClaudeCliFile('${key}', this.files[0])">
<small style="color: #a0a0a0; display: block; margin-top: 5px;">
Optional — upload a <code style="background:#0f2840;padding:1px 4px;border-radius:3px;">~/.claude/.credentials.json</code>
to use a specific account instead of your OAuth2 tokens.
</small>
</div>
<div id="claude-cli-upload-status-${key}" style="margin-top: 10px;"></div>
</div>
` : ''}
</div>
<div style="background: #ffdd57; border: 2px solid #ff6b35; border-radius: 8px; padding: 20px; margin-bottom: 15px;">
<h4 style="margin: 0 0 10px 0; color: #d63031;">⚠️ Important Notice</h4>
<p style="margin: 0; color: #2d3436;">
Claude.ai policies state that unofficial clients are not allowed. By using AISBF as a client, you acknowledge that:
Claude.ai policies state that unofficial clients are not allowed. By using AISBF in <strong>HTTP API / OAuth2 mode</strong>, you acknowledge that:
<br><br>• While we do our best to mimic the claude-code CLI, using an unofficial client can lead to account suspension or cancellation
<br>• Claude.ai detects prompt patterns to identify unofficial client usage, so it may not work with complex agents and prompts
<br>• You use this software at your own risk and responsibility
<br>• We are not affiliated with Anthropic and cannot guarantee compatibility or continued functionality
<br><br><strong>CLI mode is different:</strong> when "Use Claude CLI mode" is enabled, AISBF proxies requests through the official <code style="background:#e8e0b0;padding:1px 4px;border-radius:3px;">claude</code> binary using <code style="background:#e8e0b0;padding:1px 4px;border-radius:3px;">claude -p</code>, which is the intended programmatic use of the official Anthropic CLI and is permitted by Claude's terms of service.
</p>
</div>
`;
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment