diff --git a/.DS_Store b/.DS_Store deleted file mode 100644 index f86e442b..00000000 Binary files a/.DS_Store and /dev/null differ diff --git a/.env.example b/.env.example index e40d5f44..ff350046 100644 --- a/.env.example +++ b/.env.example @@ -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) # ============================================================================= diff --git a/.gitignore b/.gitignore index d3eaa410..3b34af5f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ __pycache__/ **/__pycache__/ *.py[cod] **/*.py[cod] +/\.venv/ /screenshots/ /venv/ /__pycache__/ @@ -10,6 +11,9 @@ __pycache__/ .pytest_cache/ .coverage .env +.DS_Store +*.egg-info/ +**/*.egg-info/ .clinerules/byterover-rules.md @@ -25,4 +29,3 @@ __pycache__/ /build/ /dist/ packaging/AppIcon.icns - diff --git a/README.md b/README.md index 9eff4db7..79241ff9 100644 --- a/README.md +++ b/README.md @@ -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** | | **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. @@ -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): @@ -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`) diff --git a/frontend_flutter/lib/main.dart b/frontend_flutter/lib/main.dart index c3075340..10afd0d8 100644 --- a/frontend_flutter/lib/main.dart +++ b/frontend_flutter/lib/main.dart @@ -50,6 +50,12 @@ Future _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, ); diff --git a/frontend_flutter/lib/src/app/config/app_config.dart b/frontend_flutter/lib/src/app/config/app_config.dart index 1218df70..35bc733d 100644 --- a/frontend_flutter/lib/src/app/config/app_config.dart +++ b/frontend_flutter/lib/src/app/config/app_config.dart @@ -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; @@ -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, }); @@ -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: { @@ -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, }) { @@ -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; diff --git a/frontend_flutter/lib/src/app/services/api_key_validator.dart b/frontend_flutter/lib/src/app/services/api_key_validator.dart index 1a99e794..952116b6 100644 --- a/frontend_flutter/lib/src/app/services/api_key_validator.dart +++ b/frontend_flutter/lib/src/app/services/api_key_validator.dart @@ -4,6 +4,7 @@ import 'package:injectable/injectable.dart'; enum ApiProvider { anthropic, openai, + azureOpenAI, } /// Result of API key validation @@ -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(); } } @@ -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.'; } } } diff --git a/frontend_flutter/lib/src/app/services/secure_storage_service.dart b/frontend_flutter/lib/src/app/services/secure_storage_service.dart index e4cc10c2..58cc3e8b 100644 --- a/frontend_flutter/lib/src/app/services/secure_storage_service.dart +++ b/frontend_flutter/lib/src/app/services/secure_storage_service.dart @@ -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'; @@ -131,6 +135,26 @@ class SecureStorageService { Future hasOpenAIApiKey() async => (await getOpenAIApiKey()) != null; Future deleteOpenAIApiKey() => _remove(_openaiApiKeyKey); + // Azure OpenAI + + Future saveAzureOpenAIApiKey(String apiKey) => + _set(_azureOpenAIApiKeyKey, apiKey); + Future getAzureOpenAIApiKey() => _get(_azureOpenAIApiKeyKey); + Future deleteAzureOpenAIApiKey() => _remove(_azureOpenAIApiKeyKey); + + Future saveAzureOpenAIEndpoint(String endpoint) => + _set(_azureOpenAIEndpointKey, endpoint); + Future getAzureOpenAIEndpoint() => _get(_azureOpenAIEndpointKey); + Future deleteAzureOpenAIEndpoint() => _remove(_azureOpenAIEndpointKey); + + Future saveAzureOpenAIDeployment(String deployment) => + _set(_azureOpenAIDeploymentKey, deployment); + Future getAzureOpenAIDeployment() => _get(_azureOpenAIDeploymentKey); + + Future saveAzureOpenAIApiVersion(String apiVersion) => + _set(_azureOpenAIApiVersionKey, apiVersion); + Future getAzureOpenAIApiVersion() => _get(_azureOpenAIApiVersionKey); + // Active provider Future saveActiveProvider(String provider) => @@ -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), }; } } diff --git a/frontend_flutter/lib/src/features/chat/presentation/widgets/chat_input_composer.dart b/frontend_flutter/lib/src/features/chat/presentation/widgets/chat_input_composer.dart index 423c7a8c..36929110 100644 --- a/frontend_flutter/lib/src/features/chat/presentation/widgets/chat_input_composer.dart +++ b/frontend_flutter/lib/src/features/chat/presentation/widgets/chat_input_composer.dart @@ -57,12 +57,22 @@ class _ChatInputComposerState extends State { 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(); - 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'; } diff --git a/frontend_flutter/lib/src/presentation/settings/first_run_dialog.dart b/frontend_flutter/lib/src/presentation/settings/first_run_dialog.dart index 1f859c0b..0b2d2717 100644 --- a/frontend_flutter/lib/src/presentation/settings/first_run_dialog.dart +++ b/frontend_flutter/lib/src/presentation/settings/first_run_dialog.dart @@ -17,11 +17,25 @@ class FirstRunDialog extends StatefulWidget { class _FirstRunDialogState extends State { final _formKey = GlobalKey(); final _storage = GetIt.I(); + final _azureEndpointController = TextEditingController(); + final _azureDeploymentController = + TextEditingController(text: 'computer-use-preview'); + final _azureApiVersionController = + TextEditingController(text: '2025-04-01-preview'); String _selectedProvider = 'openai'; String _anthropicKey = ''; String _openaiKey = ''; + String _azureOpenAIKey = ''; bool _isLoading = false; + @override + void dispose() { + _azureEndpointController.dispose(); + _azureDeploymentController.dispose(); + _azureApiVersionController.dispose(); + super.dispose(); + } + Future _getStarted() async { if (!_formKey.currentState!.validate()) return; @@ -34,6 +48,21 @@ class _FirstRunDialogState extends State { if (_openaiKey.isNotEmpty) { await _storage.saveOpenAIApiKey(_openaiKey); } + if (_azureOpenAIKey.isNotEmpty) { + await _storage.saveAzureOpenAIApiKey(_azureOpenAIKey); + } + final azureEndpoint = _azureEndpointController.text.trim(); + if (azureEndpoint.isNotEmpty) { + await _storage.saveAzureOpenAIEndpoint(azureEndpoint); + } + await _storage.saveAzureOpenAIDeployment( + _azureDeploymentController.text.trim().isEmpty + ? 'computer-use-preview' + : _azureDeploymentController.text.trim()); + await _storage.saveAzureOpenAIApiVersion( + _azureApiVersionController.text.trim().isEmpty + ? '2025-04-01-preview' + : _azureApiVersionController.text.trim()); await _storage.saveActiveProvider(_selectedProvider); await _storage.markSetupComplete(); @@ -42,6 +71,15 @@ class _FirstRunDialogState extends State { config.update( anthropicApiKey: _anthropicKey.isEmpty ? null : _anthropicKey, openaiApiKey: _openaiKey.isEmpty ? null : _openaiKey, + azureOpenAIApiKey: + _azureOpenAIKey.isEmpty ? null : _azureOpenAIKey, + azureOpenAIEndpoint: azureEndpoint.isEmpty ? null : azureEndpoint, + azureOpenAIDeployment: _azureDeploymentController.text.trim().isEmpty + ? 'computer-use-preview' + : _azureDeploymentController.text.trim(), + azureOpenAIApiVersion: _azureApiVersionController.text.trim().isEmpty + ? '2025-04-01-preview' + : _azureApiVersionController.text.trim(), activeProvider: _selectedProvider, ); @@ -62,6 +100,7 @@ class _FirstRunDialogState extends State { @override Widget build(BuildContext context) { final isAnthropic = _selectedProvider == 'anthropic'; + final isAzureOpenAI = _selectedProvider == 'azure_openai'; return Dialog( child: Container( @@ -102,6 +141,10 @@ class _FirstRunDialogState extends State { value: 'openai', label: Text('OpenAI'), icon: Icon(Icons.bolt)), + ButtonSegment( + value: 'azure_openai', + label: Text('Azure OpenAI'), + icon: Icon(Icons.cloud)), ], selected: {_selectedProvider}, onSelectionChanged: (v) => @@ -139,7 +182,9 @@ class _FirstRunDialogState extends State { Text( isAnthropic ? '1. Visit console.anthropic.com' - : '1. Visit platform.openai.com', + : isAzureOpenAI + ? '1. Open your Azure OpenAI resource' + : '1. Visit platform.openai.com', style: TextStyle( color: Theme.of(context) .colorScheme @@ -165,7 +210,9 @@ class _FirstRunDialogState extends State { onTap: () => _launchUrl( isAnthropic ? 'https://console.anthropic.com/' - : 'https://platform.openai.com/api-keys', + : isAzureOpenAI + ? 'https://portal.azure.com/' + : 'https://platform.openai.com/api-keys', ), child: Row( children: [ @@ -178,7 +225,9 @@ class _FirstRunDialogState extends State { Text( isAnthropic ? 'Open Anthropic Console' - : 'Open OpenAI Platform', + : isAzureOpenAI + ? 'Open Azure Portal' + : 'Open OpenAI Platform', style: TextStyle( color: Theme.of(context).colorScheme.primary, @@ -205,6 +254,51 @@ class _FirstRunDialogState extends State { required: true, onChanged: (value) => _anthropicKey = value, ) + else if (isAzureOpenAI) ...[ + ApiKeyField( + label: 'Azure OpenAI API Key', + hint: 'Azure OpenAI key', + provider: ApiProvider.azureOpenAI, + required: true, + onChanged: (value) => _azureOpenAIKey = value, + ), + const SizedBox(height: 16), + TextFormField( + controller: _azureEndpointController, + decoration: const InputDecoration( + labelText: 'Azure OpenAI Endpoint', + hintText: 'https://your-resource.openai.azure.com', + border: OutlineInputBorder(), + ), + validator: (value) { + final text = (value ?? '').trim(); + if (text.isEmpty) return 'Azure OpenAI endpoint is required'; + final uri = Uri.tryParse(text); + if (uri == null || !uri.hasScheme || uri.host.isEmpty) { + return 'Enter a valid endpoint URL'; + } + return null; + }, + ), + const SizedBox(height: 16), + TextFormField( + controller: _azureDeploymentController, + decoration: const InputDecoration( + labelText: 'Azure OpenAI Deployment', + hintText: 'computer-use-preview', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 16), + TextFormField( + controller: _azureApiVersionController, + decoration: const InputDecoration( + labelText: 'Azure OpenAI API Version', + hintText: '2025-04-01-preview', + border: OutlineInputBorder(), + ), + ), + ] else ApiKeyField( label: 'OpenAI API Key', diff --git a/frontend_flutter/lib/src/presentation/settings/settings_screen.dart b/frontend_flutter/lib/src/presentation/settings/settings_screen.dart index d9c59bf5..83cb938e 100644 --- a/frontend_flutter/lib/src/presentation/settings/settings_screen.dart +++ b/frontend_flutter/lib/src/presentation/settings/settings_screen.dart @@ -25,8 +25,12 @@ class _SettingsScreenState extends State { late TextEditingController _hostController; late TextEditingController _portController; late TextEditingController _preferencesController; + late TextEditingController _azureEndpointController; + late TextEditingController _azureDeploymentController; + late TextEditingController _azureApiVersionController; String _anthropicKey = ''; String _openaiKey = ''; + String _azureOpenAIKey = ''; String _activeProvider = 'anthropic'; bool _isLoading = false; @@ -40,6 +44,12 @@ class _SettingsScreenState extends State { _portController = TextEditingController(text: config.port.toString()); _preferencesController = TextEditingController(text: config.userPreferences ?? ''); + _azureEndpointController = + TextEditingController(text: config.azureOpenAIEndpoint ?? ''); + _azureDeploymentController = TextEditingController( + text: config.azureOpenAIDeployment ?? 'computer-use-preview'); + _azureApiVersionController = TextEditingController( + text: config.azureOpenAIApiVersion ?? '2025-04-01-preview'); _loadSavedKeys(); } @@ -53,6 +63,12 @@ class _SettingsScreenState extends State { setState(() { _anthropicKey = keys['anthropic'] ?? ''; _openaiKey = keys['openai'] ?? ''; + _azureOpenAIKey = keys['azure_openai'] ?? ''; + _azureEndpointController.text = keys['azure_openai_endpoint'] ?? ''; + _azureDeploymentController.text = + keys['azure_openai_deployment'] ?? 'computer-use-preview'; + _azureApiVersionController.text = + keys['azure_openai_api_version'] ?? '2025-04-01-preview'; if (savedProvider != null) { _activeProvider = savedProvider; } else { @@ -72,6 +88,8 @@ class _SettingsScreenState extends State { void _autoDetectProvider() { if (_openaiKey.isNotEmpty && _anthropicKey.isEmpty) { _activeProvider = 'openai'; + } else if (_azureOpenAIKey.isNotEmpty && _anthropicKey.isEmpty) { + _activeProvider = 'azure_openai'; } else { _activeProvider = 'anthropic'; } @@ -82,6 +100,9 @@ class _SettingsScreenState extends State { _hostController.dispose(); _portController.dispose(); _preferencesController.dispose(); + _azureEndpointController.dispose(); + _azureDeploymentController.dispose(); + _azureApiVersionController.dispose(); super.dispose(); } @@ -104,6 +125,25 @@ class _SettingsScreenState extends State { } else { await _storage.deleteOpenAIApiKey(); } + if (_azureOpenAIKey.isNotEmpty) { + await _storage.saveAzureOpenAIApiKey(_azureOpenAIKey); + } else { + await _storage.deleteAzureOpenAIApiKey(); + } + final azureEndpoint = _azureEndpointController.text.trim(); + if (azureEndpoint.isNotEmpty) { + await _storage.saveAzureOpenAIEndpoint(azureEndpoint); + } else { + await _storage.deleteAzureOpenAIEndpoint(); + } + await _storage.saveAzureOpenAIDeployment( + _azureDeploymentController.text.trim().isEmpty + ? 'computer-use-preview' + : _azureDeploymentController.text.trim()); + await _storage.saveAzureOpenAIApiVersion( + _azureApiVersionController.text.trim().isEmpty + ? '2025-04-01-preview' + : _azureApiVersionController.text.trim()); await _storage.saveActiveProvider(_activeProvider); // Save user preferences @@ -126,6 +166,14 @@ class _SettingsScreenState extends State { port: int.tryParse(_portController.text), anthropicApiKey: _anthropicKey, openaiApiKey: _openaiKey, + azureOpenAIApiKey: _azureOpenAIKey, + azureOpenAIEndpoint: azureEndpoint, + azureOpenAIDeployment: _azureDeploymentController.text.trim().isEmpty + ? 'computer-use-preview' + : _azureDeploymentController.text.trim(), + azureOpenAIApiVersion: _azureApiVersionController.text.trim().isEmpty + ? '2025-04-01-preview' + : _azureApiVersionController.text.trim(), activeProvider: _activeProvider, userPreferences: prefs.isEmpty ? '' : prefs, ); @@ -278,6 +326,11 @@ class _SettingsScreenState extends State { 'https://platform.openai.com/api-keys', Icons.launch, ), + _buildLinkItem( + 'Azure Portal', + 'https://portal.azure.com/', + Icons.launch, + ), ], ), const SizedBox(height: 16), @@ -304,6 +357,53 @@ class _SettingsScreenState extends State { ), const SizedBox(height: 24), + ApiKeyField( + label: 'Azure OpenAI API Key', + hint: 'Azure OpenAI key', + initialValue: _azureOpenAIKey, + provider: ApiProvider.azureOpenAI, + required: _activeProvider == 'azure_openai', + onChanged: (value) => _azureOpenAIKey = value, + ), + const SizedBox(height: 16), + TextFormField( + controller: _azureEndpointController, + decoration: const InputDecoration( + labelText: 'Azure OpenAI Endpoint', + hintText: 'https://your-resource.openai.azure.com', + border: OutlineInputBorder(), + ), + validator: (value) { + if (_activeProvider != 'azure_openai') return null; + final text = (value ?? '').trim(); + if (text.isEmpty) return 'Azure OpenAI endpoint is required'; + final uri = Uri.tryParse(text); + if (uri == null || !uri.hasScheme || uri.host.isEmpty) { + return 'Enter a valid endpoint URL'; + } + return null; + }, + ), + const SizedBox(height: 16), + TextFormField( + controller: _azureDeploymentController, + decoration: const InputDecoration( + labelText: 'Azure OpenAI Deployment', + hintText: 'computer-use-preview', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 16), + TextFormField( + controller: _azureApiVersionController, + decoration: const InputDecoration( + labelText: 'Azure OpenAI API Version', + hintText: '2025-04-01-preview', + border: OutlineInputBorder(), + ), + ), + const SizedBox(height: 24), + // Provider Selector _buildSectionHeader('Active Provider', Icons.smart_toy), const SizedBox(height: 8), @@ -441,6 +541,8 @@ class _SettingsScreenState extends State { Widget _buildProviderSelector() { final hasAnthropic = _anthropicKey.isNotEmpty; final hasOpenai = _openaiKey.isNotEmpty; + final hasAzureOpenAI = + _azureOpenAIKey.isNotEmpty && _azureEndpointController.text.isNotEmpty; final colorScheme = Theme.of(context).colorScheme; return Card( @@ -486,13 +588,31 @@ class _SettingsScreenState extends State { ), icon: const Icon(Icons.bolt), ), + ButtonSegment( + value: 'azure_openai', + label: Row( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('Azure OpenAI'), + const SizedBox(width: 6), + if (hasAzureOpenAI) + Icon(Icons.check_circle, + color: colorScheme.primary, size: 16) + else + Icon(Icons.warning_amber, + color: colorScheme.error, size: 16), + ], + ), + icon: const Icon(Icons.cloud), + ), ], selected: {_activeProvider}, onSelectionChanged: (v) => setState(() => _activeProvider = v.first), ), if ((_activeProvider == 'anthropic' && !hasAnthropic) || - (_activeProvider == 'openai' && !hasOpenai)) + (_activeProvider == 'openai' && !hasOpenai) || + (_activeProvider == 'azure_openai' && !hasAzureOpenAI)) Padding( padding: const EdgeInsets.only(top: 8.0), child: Text( diff --git a/frontend_flutter/lib/src/presentation/settings/widgets/api_key_field.dart b/frontend_flutter/lib/src/presentation/settings/widgets/api_key_field.dart index 0f99c6da..3f576d46 100644 --- a/frontend_flutter/lib/src/presentation/settings/widgets/api_key_field.dart +++ b/frontend_flutter/lib/src/presentation/settings/widgets/api_key_field.dart @@ -155,6 +155,8 @@ class _ApiKeyFieldState extends State { return 'sk-ant-...'; case ApiProvider.openai: return 'sk-...'; + case ApiProvider.azureOpenAI: + return 'Azure OpenAI key'; } } } @@ -166,6 +168,8 @@ extension on ApiKeyValidator { return validateAnthropicKey(value); case ApiProvider.openai: return validateOpenAIKey(value); + case ApiProvider.azureOpenAI: + return validate(value, expectedProvider: ApiProvider.azureOpenAI); } } } diff --git a/packages/backend/src/os_ai_backend.egg-info/PKG-INFO b/packages/backend/src/os_ai_backend.egg-info/PKG-INFO index fadce73a..4ae24d94 100644 --- a/packages/backend/src/os_ai_backend.egg-info/PKG-INFO +++ b/packages/backend/src/os_ai_backend.egg-info/PKG-INFO @@ -11,6 +11,9 @@ Requires-Dist: pydantic>=2.8 Requires-Dist: python-multipart>=0.0.9 Requires-Dist: orjson>=3.10.7; platform_system != "Windows" Requires-Dist: os_ai_core>=0.1.0 +Requires-Dist: os_ai_llm_anthropic>=0.1.0 +Requires-Dist: os_ai_llm_openai>=0.1.0 +Requires-Dist: os_ai_mcp>=0.1.0 OS AI Backend ================= diff --git a/packages/backend/src/os_ai_backend.egg-info/SOURCES.txt b/packages/backend/src/os_ai_backend.egg-info/SOURCES.txt index 036d9837..0d445c03 100644 --- a/packages/backend/src/os_ai_backend.egg-info/SOURCES.txt +++ b/packages/backend/src/os_ai_backend.egg-info/SOURCES.txt @@ -11,6 +11,8 @@ src/os_ai_backend/security.py src/os_ai_backend/sessions.py src/os_ai_backend/settings.py src/os_ai_backend/ws.py +src/os_ai_backend/ws_approval.py +src/os_ai_backend/ws_event_sink.py src/os_ai_backend.egg-info/PKG-INFO src/os_ai_backend.egg-info/SOURCES.txt src/os_ai_backend.egg-info/dependency_links.txt diff --git a/packages/backend/src/os_ai_backend.egg-info/requires.txt b/packages/backend/src/os_ai_backend.egg-info/requires.txt index 664c5150..06ac6e4f 100644 --- a/packages/backend/src/os_ai_backend.egg-info/requires.txt +++ b/packages/backend/src/os_ai_backend.egg-info/requires.txt @@ -3,6 +3,9 @@ uvicorn[standard]>=0.30 pydantic>=2.8 python-multipart>=0.0.9 os_ai_core>=0.1.0 +os_ai_llm_anthropic>=0.1.0 +os_ai_llm_openai>=0.1.0 +os_ai_mcp>=0.1.0 [:platform_system != "Windows"] orjson>=3.10.7 diff --git a/packages/backend/src/os_ai_backend/ws.py b/packages/backend/src/os_ai_backend/ws.py index 61528643..66143095 100644 --- a/packages/backend/src/os_ai_backend/ws.py +++ b/packages/backend/src/os_ai_backend/ws.py @@ -29,7 +29,7 @@ from os_ai_llm.config import LLM_PROVIDER as _DEFAULT_PROVIDER from .ws_approval import WebSocketApprovalAdapter from .ws_event_sink import WebSocketEventSink -_PROVIDER_DISPLAY = {"anthropic": "Anthropic", "openai": "OpenAI"} +_PROVIDER_DISPLAY = {"anthropic": "Anthropic", "openai": "OpenAI", "azure_openai": "Azure OpenAI"} # pyautogui не является thread-safe - только один agent.run одновременно. _agent_run_lock = threading.Lock() @@ -60,6 +60,14 @@ async def handle(self, websocket: WebSocket) -> None: api_keys = { 'anthropic': query_params.get('anthropic_api_key'), 'openai': query_params.get('openai_api_key'), + 'azure_openai': query_params.get('azure_openai_api_key'), + } + provider_options = { + "azure_openai": { + "endpoint": query_params.get("azure_openai_endpoint"), + "deployment": query_params.get("azure_openai_deployment"), + "api_version": query_params.get("azure_openai_api_version"), + } } # Legacy: single api_key param legacy_key = query_params.get('api_key') @@ -73,6 +81,17 @@ def get_api_key(provider: str | None) -> str | None: # Fallback to legacy single key return legacy_key + def get_provider_options(provider: str | None) -> dict[str, dict[str, str | None]] | None: + p = provider or _DEFAULT_PROVIDER + opts = provider_options.get(p, {}) + return provider_options if any(v for v in opts.values()) else None + + def create_session_for_provider(provider: str | None): + options = get_provider_options(provider) + if options is None: + return self._create_session(provider, api_key=get_api_key(provider)) + return self._create_session(provider, api_key=get_api_key(provider), provider_options=options) + if any(v for v in api_keys.values() if v): self._logger.info("API keys provided via WebSocket query params") else: @@ -101,7 +120,7 @@ def get_api_key(provider: str | None) -> str | None: provider_display = _PROVIDER_DISPLAY.get(provider or _DEFAULT_PROVIDER, (provider or _DEFAULT_PROVIDER).title()) try: session_id, client, tools, tool_gateway = self._normalize_session( - self._create_session(provider, api_key=get_api_key(provider)) + create_session_for_provider(provider) ) self._logger.info("session.create -> %s (provider=%s)", session_id, provider or "default") await self._send_result(websocket, req_id, { @@ -130,7 +149,7 @@ def get_api_key(provider: str | None) -> str | None: # Build session and run orchestration in background try: session_id, client, tools, tool_gateway = self._normalize_session( - self._create_session(provider, api_key=get_api_key(provider)) + create_session_for_provider(provider) ) except RuntimeError as e: self._logger.warning( @@ -358,8 +377,20 @@ def _locked_blocking_run(): await self._send_event(websocket, "event.final", {"jobId": job_id, **result}) self._logger.info("agent.run completed job=%s status=%s", job_id, result.get("status")) - def _create_session(self, provider: Optional[str], api_key: Optional[str] = None) -> tuple[str, LLMClient, ToolRegistry, CompositeToolGateway]: - inj = _create_container(provider, api_key=api_key) + def _create_session( + self, + provider: Optional[str], + api_key: Optional[str] = None, + provider_options: Optional[dict[str, dict[str, str | None]]] = None, + ) -> tuple[str, LLMClient, ToolRegistry, CompositeToolGateway]: + p = provider or _DEFAULT_PROVIDER + provider_options = provider_options or {} + options = { + k: v + for k, v in provider_options.get(p, {}).items() + if isinstance(v, str) and v + } + inj = _create_container(provider, api_key=api_key, provider_options=options) client = inj.get(LLMClient) tools = inj.get(ToolRegistry) try: @@ -400,10 +431,10 @@ def _dumps(self, obj: Any) -> str: -def _create_container(provider: Optional[str] = None, api_key: Optional[str] = None): +def _create_container(provider: Optional[str] = None, api_key: Optional[str] = None, provider_options: Optional[dict[str, str]] = None): # Lazy import to avoid hard dependency at import time (helps tests/CI without injector installed) from os_ai_core.di import create_container as _cc # type: ignore - return _cc(provider, api_key=api_key) + return _cc(provider, api_key=api_key, provider_options=provider_options) def _application_runner_enabled() -> bool: diff --git a/packages/core/src/os_ai_core.egg-info/SOURCES.txt b/packages/core/src/os_ai_core.egg-info/SOURCES.txt index 8556f800..f5fe7651 100644 --- a/packages/core/src/os_ai_core.egg-info/SOURCES.txt +++ b/packages/core/src/os_ai_core.egg-info/SOURCES.txt @@ -1,6 +1,14 @@ README.md pyproject.toml src/os_ai_core/__init__.py +src/os_ai_core/config.py +src/os_ai_core/di.py +src/os_ai_core/orchestrator.py +src/os_ai_core.egg-info/PKG-INFO +src/os_ai_core.egg-info/SOURCES.txt +src/os_ai_core.egg-info/dependency_links.txt +src/os_ai_core.egg-info/requires.txt +src/os_ai_core.egg-info/top_level.txt src/os_ai_core/adapters/__init__.py src/os_ai_core/adapters/approval.py src/os_ai_core/adapters/events/__init__.py @@ -32,8 +40,6 @@ src/os_ai_core/application/services/provider_safety.py src/os_ai_core/application/services/tool_execution_ledger.py src/os_ai_core/application/use_cases/__init__.py src/os_ai_core/application/use_cases/run_agent.py -src/os_ai_core/config.py -src/os_ai_core/di.py src/os_ai_core/domain/__init__.py src/os_ai_core/domain/agent/__init__.py src/os_ai_core/domain/agent/events.py @@ -45,16 +51,10 @@ src/os_ai_core/domain/tools/errors.py src/os_ai_core/domain/tools/execution.py src/os_ai_core/domain/tools/models.py src/os_ai_core/domain/tools/policies.py -src/os_ai_core/orchestrator.py src/os_ai_core/tools/__init__.py src/os_ai_core/tools/computer.py src/os_ai_core/tools/registry.py src/os_ai_core/utils/__init__.py src/os_ai_core/utils/conversation_optimizer.py src/os_ai_core/utils/costs.py -src/os_ai_core/utils/logger.py -src/os_ai_core.egg-info/PKG-INFO -src/os_ai_core.egg-info/SOURCES.txt -src/os_ai_core.egg-info/dependency_links.txt -src/os_ai_core.egg-info/requires.txt -src/os_ai_core.egg-info/top_level.txt +src/os_ai_core/utils/logger.py \ No newline at end of file diff --git a/packages/core/src/os_ai_core.egg-info/requires.txt b/packages/core/src/os_ai_core.egg-info/requires.txt index db4575d3..3fb8cb11 100644 --- a/packages/core/src/os_ai_core.egg-info/requires.txt +++ b/packages/core/src/os_ai_core.egg-info/requires.txt @@ -7,8 +7,8 @@ os_ai_os>=0.1.0 [:platform_system == "Darwin"] os_ai_os_macos>=0.1.0 -[:platform_system == "Windows"] -os_ai_os_windows>=0.1.0 - [:platform_system == "Linux"] os_ai_os_linux>=0.1.0 + +[:platform_system == "Windows"] +os_ai_os_windows>=0.1.0 diff --git a/packages/core/src/os_ai_core/di.py b/packages/core/src/os_ai_core/di.py index 0e6c7369..f1adef21 100644 --- a/packages/core/src/os_ai_core/di.py +++ b/packages/core/src/os_ai_core/di.py @@ -17,20 +17,34 @@ class LLMModule(injector.Module): - def __init__(self, provider: Optional[str] = None, api_key: Optional[str] = None) -> None: + def __init__( + self, + provider: Optional[str] = None, + api_key: Optional[str] = None, + provider_options: Optional[dict[str, str]] = None, + ) -> None: self._provider = (provider or LLM_PROVIDER).lower() self._api_key = api_key + self._provider_options = provider_options or {} @injector.provider def provide_llm_client(self) -> LLMClient: # type: ignore[override] if self._provider == "openai": from os_ai_llm_openai.adapters_openai import OpenAIClient return OpenAIClient(api_key=self._api_key) + elif self._provider == "azure_openai": + from os_ai_llm_openai.adapters_openai import AzureOpenAIClient + return AzureOpenAIClient( + api_key=self._api_key, + model_name=self._provider_options.get("deployment"), + azure_endpoint=self._provider_options.get("endpoint"), + api_version=self._provider_options.get("api_version"), + ) elif self._provider == "anthropic": from os_ai_llm_anthropic.adapters_anthropic import AnthropicClient return AnthropicClient(api_key=self._api_key) else: - raise ValueError(f"Unknown LLM provider: '{self._provider}'. Supported: 'anthropic', 'openai'") + raise ValueError(f"Unknown LLM provider: '{self._provider}'. Supported: 'anthropic', 'openai', 'azure_openai'") class ToolsModule(injector.Module): @@ -65,5 +79,9 @@ def provide_tool_gateway(self, tool_registry: ToolRegistry) -> CompositeToolGate return CompositeToolGateway(providers) -def create_container(provider: Optional[str] = None, api_key: Optional[str] = None) -> injector.Injector: - return injector.Injector([LLMModule(provider, api_key=api_key), ToolsModule(provider)]) +def create_container( + provider: Optional[str] = None, + api_key: Optional[str] = None, + provider_options: Optional[dict[str, str]] = None, +) -> injector.Injector: + return injector.Injector([LLMModule(provider, api_key=api_key, provider_options=provider_options), ToolsModule(provider)]) diff --git a/packages/llm/src/os_ai_llm.egg-info/SOURCES.txt b/packages/llm/src/os_ai_llm.egg-info/SOURCES.txt index 7ac816ab..983a27e3 100644 --- a/packages/llm/src/os_ai_llm.egg-info/SOURCES.txt +++ b/packages/llm/src/os_ai_llm.egg-info/SOURCES.txt @@ -1,5 +1,6 @@ README.md pyproject.toml +src/os_ai_llm/__init__.py src/os_ai_llm/config.py src/os_ai_llm/interfaces.py src/os_ai_llm/types.py diff --git a/packages/llm/src/os_ai_llm/config.py b/packages/llm/src/os_ai_llm/config.py index 17f1a19e..2bf73536 100644 --- a/packages/llm/src/os_ai_llm/config.py +++ b/packages/llm/src/os_ai_llm/config.py @@ -15,4 +15,5 @@ COMPUTER_TOOL_TYPES = { "anthropic": "computer_20251124", "openai": "computer", + "azure_openai": "computer", } diff --git a/packages/llm_anthropic/src/os_ai_llm_anthropic.egg-info/SOURCES.txt b/packages/llm_anthropic/src/os_ai_llm_anthropic.egg-info/SOURCES.txt index 87ee874a..feed5251 100644 --- a/packages/llm_anthropic/src/os_ai_llm_anthropic.egg-info/SOURCES.txt +++ b/packages/llm_anthropic/src/os_ai_llm_anthropic.egg-info/SOURCES.txt @@ -1,5 +1,6 @@ README.md pyproject.toml +src/os_ai_llm_anthropic/__init__.py src/os_ai_llm_anthropic/adapters_anthropic.py src/os_ai_llm_anthropic/config.py src/os_ai_llm_anthropic.egg-info/PKG-INFO diff --git a/packages/llm_openai/src/os_ai_llm_openai/__init__.py b/packages/llm_openai/src/os_ai_llm_openai/__init__.py index bc1acad3..50c91e9a 100644 --- a/packages/llm_openai/src/os_ai_llm_openai/__init__.py +++ b/packages/llm_openai/src/os_ai_llm_openai/__init__.py @@ -1,5 +1,5 @@ -from os_ai_llm_openai.adapters_openai import OpenAIClient -from os_ai_llm_openai.config import OPENAI_MODEL_NAME +from os_ai_llm_openai.adapters_openai import AzureOpenAIClient, OpenAIClient +from os_ai_llm_openai.config import AZURE_OPENAI_MODEL_NAME, OPENAI_MODEL_NAME from os_ai_llm_openai.action_converter import ( openai_action_to_internal, openai_actions_to_internal, @@ -9,7 +9,9 @@ __all__ = [ "OpenAIClient", + "AzureOpenAIClient", "OPENAI_MODEL_NAME", + "AZURE_OPENAI_MODEL_NAME", "openai_action_to_internal", "openai_actions_to_internal", "openai_keys_to_xdotool", diff --git a/packages/llm_openai/src/os_ai_llm_openai/adapters_openai.py b/packages/llm_openai/src/os_ai_llm_openai/adapters_openai.py index 95758309..8313aa99 100644 --- a/packages/llm_openai/src/os_ai_llm_openai/adapters_openai.py +++ b/packages/llm_openai/src/os_ai_llm_openai/adapters_openai.py @@ -13,9 +13,11 @@ from typing import Any, Dict, List, Optional import httpx -from openai import OpenAI, RateLimitError, APIStatusError, APIConnectionError, APITimeoutError +from openai import AzureOpenAI, OpenAI, RateLimitError, APIStatusError, APIConnectionError, APITimeoutError from os_ai_llm_openai.config import ( + AZURE_OPENAI_API_VERSION, + AZURE_OPENAI_MODEL_NAME, OPENAI_MODEL_NAME, OPENAI_API_TIMEOUT_SECONDS, OPENAI_API_MAX_RETRIES, @@ -430,3 +432,46 @@ def _tool_result_to_text(result: ToolResult) -> str: if not parts: return "success" if not result.is_error else "error" return "\n".join(parts) + + +class AzureOpenAIClient(OpenAIClient): + """Azure OpenAI Computer Use adapter via Responses API. + + Azure OpenAI uses a deployment name in the Responses API ``model`` field. + Configure it with AZURE_OPENAI_DEPLOYMENT; this app defaults to the + requested ``computer-use-preview`` deployment name. + """ + + def __init__( + self, + api_key: Optional[str] = None, + model_name: Optional[str] = None, + azure_endpoint: Optional[str] = None, + api_version: Optional[str] = None, + ) -> None: + key = api_key or os.environ.get("AZURE_OPENAI_API_KEY") + endpoint = azure_endpoint or os.environ.get("AZURE_OPENAI_ENDPOINT") + version = api_version or AZURE_OPENAI_API_VERSION + if not key: + raise RuntimeError( + "AZURE_OPENAI_API_KEY is not set. " + "Provide it via the app Settings or set the AZURE_OPENAI_API_KEY environment variable." + ) + if not endpoint: + raise RuntimeError( + "AZURE_OPENAI_ENDPOINT is not set. " + "Provide your Azure OpenAI resource endpoint in Settings or set AZURE_OPENAI_ENDPOINT." + ) + self._client = AzureOpenAI( + api_key=key, + azure_endpoint=endpoint, + api_version=version, + timeout=httpx.Timeout(float(OPENAI_API_TIMEOUT_SECONDS)), + max_retries=OPENAI_API_MAX_RETRIES, + ) + self._model = model_name or AZURE_OPENAI_MODEL_NAME + self._azure_endpoint = endpoint + self._azure_api_version = version + + def get_provider_name(self) -> str: + return "azure_openai" diff --git a/packages/llm_openai/src/os_ai_llm_openai/config.py b/packages/llm_openai/src/os_ai_llm_openai/config.py index b7c569b2..0ff69837 100644 --- a/packages/llm_openai/src/os_ai_llm_openai/config.py +++ b/packages/llm_openai/src/os_ai_llm_openai/config.py @@ -1,6 +1,8 @@ import os OPENAI_MODEL_NAME = "gpt-5.4" +AZURE_OPENAI_MODEL_NAME = os.environ.get("AZURE_OPENAI_DEPLOYMENT", "computer-use-preview") +AZURE_OPENAI_API_VERSION = os.environ.get("AZURE_OPENAI_API_VERSION", "2025-04-01-preview") OPENAI_API_TIMEOUT_SECONDS = 300 OPENAI_API_MAX_RETRIES = 3 OPENAI_REASONING_SUMMARY = "auto" diff --git a/packages/os-linux/src/os_ai_os_linux.egg-info/PKG-INFO b/packages/os-linux/src/os_ai_os_linux.egg-info/PKG-INFO new file mode 100644 index 00000000..4052e7e4 --- /dev/null +++ b/packages/os-linux/src/os_ai_os_linux.egg-info/PKG-INFO @@ -0,0 +1,10 @@ +Metadata-Version: 2.4 +Name: os_ai_os_linux +Version: 0.1.0 +Summary: Linux drivers for os_ai_os ports +Classifier: Programming Language :: Python :: 3 +Requires-Python: >=3.10 +Description-Content-Type: text/markdown +Requires-Dist: pyautogui>=0.9.54 +Requires-Dist: Pillow>=10 +Requires-Dist: os_ai_os>=0.1.0 diff --git a/packages/os-linux/src/os_ai_os_linux.egg-info/SOURCES.txt b/packages/os-linux/src/os_ai_os_linux.egg-info/SOURCES.txt new file mode 100644 index 00000000..8cd6dbd8 --- /dev/null +++ b/packages/os-linux/src/os_ai_os_linux.egg-info/SOURCES.txt @@ -0,0 +1,9 @@ +pyproject.toml +src/os_ai_os_linux/__init__.py +src/os_ai_os_linux/drivers.py +src/os_ai_os_linux.egg-info/PKG-INFO +src/os_ai_os_linux.egg-info/SOURCES.txt +src/os_ai_os_linux.egg-info/dependency_links.txt +src/os_ai_os_linux.egg-info/entry_points.txt +src/os_ai_os_linux.egg-info/requires.txt +src/os_ai_os_linux.egg-info/top_level.txt \ No newline at end of file diff --git a/packages/os-linux/src/os_ai_os_linux.egg-info/dependency_links.txt b/packages/os-linux/src/os_ai_os_linux.egg-info/dependency_links.txt new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/packages/os-linux/src/os_ai_os_linux.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/packages/os-linux/src/os_ai_os_linux.egg-info/entry_points.txt b/packages/os-linux/src/os_ai_os_linux.egg-info/entry_points.txt new file mode 100644 index 00000000..7a44e5e5 --- /dev/null +++ b/packages/os-linux/src/os_ai_os_linux.egg-info/entry_points.txt @@ -0,0 +1,2 @@ +[os_ai_os.drivers] +linux = os_ai_os_linux.drivers:make_drivers diff --git a/packages/os-linux/src/os_ai_os_linux.egg-info/requires.txt b/packages/os-linux/src/os_ai_os_linux.egg-info/requires.txt new file mode 100644 index 00000000..71c8c77f --- /dev/null +++ b/packages/os-linux/src/os_ai_os_linux.egg-info/requires.txt @@ -0,0 +1,3 @@ +pyautogui>=0.9.54 +Pillow>=10 +os_ai_os>=0.1.0 diff --git a/packages/os-linux/src/os_ai_os_linux.egg-info/top_level.txt b/packages/os-linux/src/os_ai_os_linux.egg-info/top_level.txt new file mode 100644 index 00000000..f2831d85 --- /dev/null +++ b/packages/os-linux/src/os_ai_os_linux.egg-info/top_level.txt @@ -0,0 +1 @@ +os_ai_os_linux diff --git a/packages/os/src/os_ai_os.egg-info/SOURCES.txt b/packages/os/src/os_ai_os.egg-info/SOURCES.txt index 26ebd253..314c4ae4 100644 --- a/packages/os/src/os_ai_os.egg-info/SOURCES.txt +++ b/packages/os/src/os_ai_os.egg-info/SOURCES.txt @@ -1,7 +1,9 @@ README.md pyproject.toml +src/os_ai_os/__init__.py src/os_ai_os/api.py src/os_ai_os/config.py +src/os_ai_os/defaults.py src/os_ai_os/virtual_mouse.py src/os_ai_os.egg-info/PKG-INFO src/os_ai_os.egg-info/SOURCES.txt diff --git a/tests/integration/test_openai_flow.py b/tests/integration/test_openai_flow.py index e29f1e1d..0cdf5918 100644 --- a/tests/integration/test_openai_flow.py +++ b/tests/integration/test_openai_flow.py @@ -9,7 +9,7 @@ from os_ai_llm.types import ( Message, TextPart, ImagePart, ToolDescriptor, ToolCall, ToolResult, ProviderPart, LLMResponse, Usage, ) -from os_ai_llm_openai.adapters_openai import OpenAIClient +from os_ai_llm_openai.adapters_openai import AzureOpenAIClient, OpenAIClient def _make_mock_response(response_id: str, output_items: list, input_tokens: int = 10, output_tokens: int = 5): @@ -71,6 +71,24 @@ def client(): yield c +def test_azure_openai_client_uses_deployment_endpoint_and_api_version(): + with patch("os_ai_llm_openai.adapters_openai.AzureOpenAI") as MockAzureOpenAI: + c = AzureOpenAIClient( + api_key="azure-key", + azure_endpoint="https://example.openai.azure.com", + model_name="computer-use-preview", + api_version="2025-04-01-preview", + ) + + assert c.get_provider_name() == "azure_openai" + assert c.get_model_name() == "computer-use-preview" + MockAzureOpenAI.assert_called_once() + kwargs = MockAzureOpenAI.call_args.kwargs + assert kwargs["api_key"] == "azure-key" + assert kwargs["azure_endpoint"] == "https://example.openai.azure.com" + assert kwargs["api_version"] == "2025-04-01-preview" + + def test_provider_context_roundtrip(client): """previous_response_id flows from response to next request.""" # First call: no provider_context