Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed .DS_Store
Binary file not shown.
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ ANTHROPIC_API_KEY=sk-ant-your-key-here
# Get your key from: https://platform.openai.com/api-keys
# OPENAI_API_KEY=sk-your-key-here

# Azure OpenAI (optional)
# Use a deployment of the computer-use-preview model.
# AZURE_OPENAI_API_KEY=your-azure-openai-key
# AZURE_OPENAI_ENDPOINT=https://your-resource.openai.azure.com
# AZURE_OPENAI_DEPLOYMENT=computer-use-preview
# AZURE_OPENAI_API_VERSION=2025-04-01-preview

# =============================================================================
# BACKEND CONFIGURATION (Optional)
# =============================================================================
Expand Down
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,17 @@ __pycache__/
**/__pycache__/
*.py[cod]
**/*.py[cod]
/\.venv/
/screenshots/
/venv/
/__pycache__/
*/__pycache__/*
.pytest_cache/
.coverage
.env
.DS_Store
*.egg-info/
**/*.egg-info/


.clinerules/byterover-rules.md
Expand All @@ -25,4 +29,3 @@ __pycache__/
/build/
/dist/
packaging/AppIcon.icns

5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ Available for:
| Provider | Model | Computer Use | Status |
|----------|-------|-------------|--------|
| **OpenAI** | GPT-5.4 | Batched actions, `previous_response_id` continuity | **Fully supported** |
| **Azure OpenAI** | `computer-use-preview` deployment | Batched actions, `previous_response_id` continuity | **Supported** |

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Update the README’s other provider summaries to include Azure OpenAI.

This table now lists Azure as supported, but nearby descriptions still say the project supports only OpenAI and Anthropic. Update those summaries so users do not receive contradictory provider guidance.

🤖 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 `@README.md` at line 44, Update the nearby README provider summaries that
currently mention only OpenAI and Anthropic to also include Azure OpenAI,
keeping the existing wording and table entry consistent without changing
unrelated documentation.

| **Anthropic** | Claude Sonnet 4.6 / Opus 4.6 | Single actions, zoom, full message history | **Fully supported** |

Switch providers in **Settings** — enter your API key and select the active provider from the dropdown.
Expand Down Expand Up @@ -101,6 +102,7 @@ Requirements:
- Python 3.12+
- API key for at least one provider:
- **OpenAI**: `OPENAI_API_KEY` (for GPT-5.4 Computer Use)
- **Azure OpenAI**: `AZURE_OPENAI_API_KEY`, `AZURE_OPENAI_ENDPOINT`, and `AZURE_OPENAI_DEPLOYMENT=computer-use-preview`
- **Anthropic**: `ANTHROPIC_API_KEY` (for Claude Computer Use)

Linux system dependencies (if applicable):
Expand Down Expand Up @@ -231,7 +233,8 @@ os-ai-backend
```

Backend environment variables (optional):
- `LLM_PROVIDER` - default AI provider: `openai` or `anthropic` (default: `anthropic`)
- `LLM_PROVIDER` - default AI provider: `openai`, `azure_openai`, or `anthropic` (default: `anthropic`)
- `AZURE_OPENAI_API_KEY`, `AZURE_OPENAI_ENDPOINT`, `AZURE_OPENAI_DEPLOYMENT`, `AZURE_OPENAI_API_VERSION` - Azure OpenAI settings when `LLM_PROVIDER=azure_openai`
- `OS_AI_BACKEND_HOST` - host address (default: `127.0.0.1`)
- `OS_AI_BACKEND_PORT` - port number (default: `8765`)
- `OS_AI_BACKEND_DEBUG` - enable debug logging (default: `0`)
Expand Down
6 changes: 6 additions & 0 deletions frontend_flutter/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ Future<AppConfig> _loadInitialConfig() async {
return AppConfig(
anthropicApiKey: keys['anthropic'],
openaiApiKey: keys['openai'],
azureOpenAIApiKey: keys['azure_openai'],
azureOpenAIEndpoint: keys['azure_openai_endpoint'],
azureOpenAIDeployment:
keys['azure_openai_deployment'] ?? 'computer-use-preview',
azureOpenAIApiVersion:
keys['azure_openai_api_version'] ?? '2025-04-01-preview',
activeProvider: savedProvider ?? 'anthropic',
userPreferences: userPreferences,
);
Expand Down
44 changes: 44 additions & 0 deletions frontend_flutter/lib/src/app/config/app_config.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ class AppConfig extends ChangeNotifier {
int historyPairsLimit;
String? anthropicApiKey;
String? openaiApiKey;
String? azureOpenAIApiKey;
String? azureOpenAIEndpoint;
String? azureOpenAIDeployment;
String? azureOpenAIApiVersion;
String activeProvider;
String? userPreferences;

Expand All @@ -17,6 +21,10 @@ class AppConfig extends ChangeNotifier {
this.historyPairsLimit = 6,
this.anthropicApiKey,
this.openaiApiKey,
this.azureOpenAIApiKey,
this.azureOpenAIEndpoint,
this.azureOpenAIDeployment = 'computer-use-preview',
this.azureOpenAIApiVersion = '2025-04-01-preview',
this.activeProvider = 'anthropic',
this.userPreferences,
});
Expand All @@ -31,6 +39,18 @@ class AppConfig extends ChangeNotifier {
if (openaiApiKey != null && openaiApiKey!.isNotEmpty) {
extra['openai_api_key'] = openaiApiKey!;
}
if (azureOpenAIApiKey != null && azureOpenAIApiKey!.isNotEmpty) {
extra['azure_openai_api_key'] = azureOpenAIApiKey!;
}
if (azureOpenAIEndpoint != null && azureOpenAIEndpoint!.isNotEmpty) {
extra['azure_openai_endpoint'] = azureOpenAIEndpoint!;
}
if (azureOpenAIDeployment != null && azureOpenAIDeployment!.isNotEmpty) {
extra['azure_openai_deployment'] = azureOpenAIDeployment!;
}
if (azureOpenAIApiVersion != null && azureOpenAIApiVersion!.isNotEmpty) {
extra['azure_openai_api_version'] = azureOpenAIApiVersion!;
}

if (extra.isNotEmpty) {
return uri.replace(queryParameters: {
Expand All @@ -51,6 +71,10 @@ class AppConfig extends ChangeNotifier {
int? historyPairsLimit,
String? anthropicApiKey,
String? openaiApiKey,
String? azureOpenAIApiKey,
String? azureOpenAIEndpoint,
String? azureOpenAIDeployment,
String? azureOpenAIApiVersion,
String? activeProvider,
String? userPreferences,
}) {
Expand Down Expand Up @@ -80,6 +104,26 @@ class AppConfig extends ChangeNotifier {
this.openaiApiKey = openaiApiKey;
changed = true;
}
if (azureOpenAIApiKey != null &&
azureOpenAIApiKey != this.azureOpenAIApiKey) {
this.azureOpenAIApiKey = azureOpenAIApiKey;
changed = true;
}
if (azureOpenAIEndpoint != null &&
azureOpenAIEndpoint != this.azureOpenAIEndpoint) {
this.azureOpenAIEndpoint = azureOpenAIEndpoint;
changed = true;
}
if (azureOpenAIDeployment != null &&
azureOpenAIDeployment != this.azureOpenAIDeployment) {
this.azureOpenAIDeployment = azureOpenAIDeployment;
changed = true;
}
if (azureOpenAIApiVersion != null &&
azureOpenAIApiVersion != this.azureOpenAIApiVersion) {
this.azureOpenAIApiVersion = azureOpenAIApiVersion;
changed = true;
}
if (activeProvider != null && activeProvider != this.activeProvider) {
this.activeProvider = activeProvider;
changed = true;
Expand Down
9 changes: 9 additions & 0 deletions frontend_flutter/lib/src/app/services/api_key_validator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import 'package:injectable/injectable.dart';
enum ApiProvider {
anthropic,
openai,
azureOpenAI,
}

/// Result of API key validation
Expand Down Expand Up @@ -98,6 +99,12 @@ class ApiKeyValidator {
'Invalid OpenAI API key format. Please check your key.');
}
return ValidationResult.valid();

case ApiProvider.azureOpenAI:
if (apiKey.length < 20) {
return ValidationResult.invalid('Azure OpenAI API key is too short');
}
return ValidationResult.valid();
}
}

Expand All @@ -120,6 +127,8 @@ class ApiKeyValidator {
case ApiProvider.openai:
return 'OpenAI API keys start with "sk-" and are typically 40-200 characters long. '
'Get your key from https://platform.openai.com/api-keys';
case ApiProvider.azureOpenAI:
return 'Azure OpenAI keys come from your Azure OpenAI resource. You also need the endpoint and deployment name.';
}
}
}
28 changes: 28 additions & 0 deletions frontend_flutter/lib/src/app/services/secure_storage_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ class SecureStorageService {
// Storage keys
static const String _anthropicApiKeyKey = 'anthropic_api_key';
static const String _openaiApiKeyKey = 'openai_api_key';
static const String _azureOpenAIApiKeyKey = 'azure_openai_api_key';
static const String _azureOpenAIEndpointKey = 'azure_openai_endpoint';
static const String _azureOpenAIDeploymentKey = 'azure_openai_deployment';
static const String _azureOpenAIApiVersionKey = 'azure_openai_api_version';
static const String _hasCompletedSetupKey = 'has_completed_setup';
static const String _activeProviderKey = 'active_provider';
static const String _userPreferencesKey = 'user_preferences';
Expand Down Expand Up @@ -131,6 +135,26 @@ class SecureStorageService {
Future<bool> hasOpenAIApiKey() async => (await getOpenAIApiKey()) != null;
Future<void> deleteOpenAIApiKey() => _remove(_openaiApiKeyKey);

// Azure OpenAI

Future<void> saveAzureOpenAIApiKey(String apiKey) =>
_set(_azureOpenAIApiKeyKey, apiKey);
Future<String?> getAzureOpenAIApiKey() => _get(_azureOpenAIApiKeyKey);
Future<void> deleteAzureOpenAIApiKey() => _remove(_azureOpenAIApiKeyKey);

Future<void> saveAzureOpenAIEndpoint(String endpoint) =>
_set(_azureOpenAIEndpointKey, endpoint);
Future<String?> getAzureOpenAIEndpoint() => _get(_azureOpenAIEndpointKey);
Future<void> deleteAzureOpenAIEndpoint() => _remove(_azureOpenAIEndpointKey);

Future<void> saveAzureOpenAIDeployment(String deployment) =>
_set(_azureOpenAIDeploymentKey, deployment);
Future<String?> getAzureOpenAIDeployment() => _get(_azureOpenAIDeploymentKey);

Future<void> saveAzureOpenAIApiVersion(String apiVersion) =>
_set(_azureOpenAIApiVersionKey, apiVersion);
Future<String?> getAzureOpenAIApiVersion() => _get(_azureOpenAIApiVersionKey);

// Active provider

Future<void> saveActiveProvider(String provider) =>
Expand Down Expand Up @@ -166,6 +190,10 @@ class SecureStorageService {
return {
'anthropic': await _get(_anthropicApiKeyKey),
'openai': await _get(_openaiApiKeyKey),
'azure_openai': await _get(_azureOpenAIApiKeyKey),
'azure_openai_endpoint': await _get(_azureOpenAIEndpointKey),
'azure_openai_deployment': await _get(_azureOpenAIDeploymentKey),
'azure_openai_api_version': await _get(_azureOpenAIApiVersionKey),
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,12 +57,22 @@ class _ChatInputComposerState extends State<ChatInputComposer> {
if (cfg.activeProvider == 'openai') {
return cfg.openaiApiKey != null && cfg.openaiApiKey!.isNotEmpty;
}
if (cfg.activeProvider == 'azure_openai') {
return cfg.azureOpenAIApiKey != null &&
cfg.azureOpenAIApiKey!.isNotEmpty &&
cfg.azureOpenAIEndpoint != null &&
cfg.azureOpenAIEndpoint!.isNotEmpty;
}
return cfg.anthropicApiKey != null && cfg.anthropicApiKey!.isNotEmpty;
}

String get _missingKeyMessage {
final cfg = context.read<AppConfig>();
final name = cfg.activeProvider == 'openai' ? 'OpenAI' : 'Anthropic';
final name = cfg.activeProvider == 'openai'
? 'OpenAI'
: cfg.activeProvider == 'azure_openai'
? 'Azure OpenAI'
: 'Anthropic';
return 'Enter your $name API key in Settings first';
}

Expand Down
Loading