├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── documentation.yml │ └── feature.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── create-release.yml │ ├── update-contributors.yml │ └── validate-pr.yml ├── .gitignore ├── .php-cs-fixer.dist.php ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── LICENSE.md ├── README.md ├── composer.json ├── examples ├── MistralAIFactory.php ├── agents │ └── createAgentsCompletion.php ├── chat │ ├── createChatCompletion.php │ └── createChatCompletionStream.php ├── embeddings │ └── createEmbedding.php ├── files │ ├── deleteFile.php │ ├── listFiles.php │ ├── retrieveFile.php │ └── uploadFile.php ├── fim │ └── createFimCompletion.php ├── fine_tuning │ ├── cancelFineTuningJob.php │ ├── createFineTuningJob.php │ ├── listFineTuningJobs.php │ ├── retrieveFineTuningJob.php │ └── startFineTuningJob.php └── models │ ├── archiveModel.php │ ├── deleteModel.php │ ├── listModels.php │ ├── retrieveModel.php │ ├── unarchiveModel.php │ └── updateFineTunedModel.php ├── phpunit.xml.dist ├── src ├── Exception │ └── MistralAIException.php ├── MistralAI.php └── MistralAIURLBuilder.php └── tests ├── MistralAITest.php ├── MistralAIURLBuilderTest.php ├── TestHelper.php └── fixtures └── responses ├── archiveModel.json ├── cancelFineTuningJob.json ├── chatCompletion.json ├── chatCompletionStreaming.json ├── createAgentsCompletion.json ├── createEmbedding.json ├── createFimCompletion.json ├── createFineTuningJob.json ├── deleteFile.json ├── deleteModel.json ├── listFiles.json ├── listFineTuningJobs.json ├── listModels.json ├── retrieveFile.json ├── retrieveFineTuningJob.json ├── retrieveModel.json ├── startFineTuningJob.json ├── unarchiveModel.json ├── updateFineTunedModel.json └── uploadFile.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: softcreatr 2 | custom: ['https://ecologi.com/softcreatr?r=61212ab3fc69b8eb8a2014f4'] 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | name: 🐛 Bug Report 2 | description: Submit a bug report to help us improve. 3 | labels: ["bug"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: "## 🐛 Bug Report" 8 | 9 | - type: textarea 10 | id: bug-description 11 | attributes: 12 | label: Description 13 | description: A clear and concise description of what the bug is. 14 | validations: 15 | required: true 16 | 17 | - type: checkboxes 18 | id: previous-research 19 | attributes: 20 | label: Have you spent some time to check if this issue has been raised before? 21 | options: 22 | - label: I have googled for a similar issue or checked our older issues for a similar bug 23 | required: true 24 | 25 | - type: checkboxes 26 | id: code-of-conduct 27 | attributes: 28 | label: Have you read the Code of Conduct? 29 | options: 30 | - label: I have read the [Code of Conduct](https://github.com/SoftCreatR/php-mistral-ai-sdk/blob/main/CODE_OF_CONDUCT.md) 31 | required: true 32 | 33 | - type: textarea 34 | id: reproduction-steps 35 | attributes: 36 | label: To Reproduce 37 | description: Write your steps here 38 | validations: 39 | required: true 40 | 41 | - type: textarea 42 | id: expected-behavior 43 | attributes: 44 | label: Expected behavior 45 | description: Write down what you thought would happen. 46 | validations: 47 | required: true 48 | 49 | - type: textarea 50 | id: actual-behavior 51 | attributes: 52 | label: Actual Behavior 53 | description: Write what happened. Add screenshots, if applicable. 54 | validations: 55 | required: true 56 | 57 | - type: textarea 58 | id: environment 59 | attributes: 60 | label: Your Environment 61 | description: Include as many relevant details about the environment you experienced the bug in (e.g., Environment, Operating system and version, etc.) 62 | validations: 63 | required: true 64 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/documentation.yml: -------------------------------------------------------------------------------- 1 | name: 📚 Documentation 2 | description: Report an issue related to documentation. 3 | labels: ["documentation"] 4 | 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: "## 📚 Documentation" 9 | 10 | - type: textarea 11 | id: issue-description 12 | attributes: 13 | label: Description 14 | description: A clear and concise description of what the issue is. 15 | placeholder: Enter the issue details here. 16 | validations: 17 | required: true 18 | 19 | - type: markdown 20 | attributes: 21 | value: "### Have you read the [Code of Conduct](https://github.com/SoftCreatR/php-mistral-ai-sdk/blob/main/CODE_OF_CONDUCT.md)?" 22 | 23 | - type: checkboxes 24 | id: code-of-conduct 25 | attributes: 26 | label: Code of Conduct 27 | description: Please confirm that you have read the Code of Conduct. 28 | options: 29 | - label: I have read the Code of Conduct 30 | required: true 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.yml: -------------------------------------------------------------------------------- 1 | name: 💡 Feature / Idea 2 | description: Submit a proposal for a new feature. 3 | labels: ["feature"] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: "## 💡 Feature / Idea" 8 | - type: textarea 9 | id: feature-description 10 | attributes: 11 | label: "A clear and concise description of what the feature is." 12 | placeholder: "Describe the feature here..." 13 | validations: 14 | required: true 15 | - type: checkboxes 16 | id: checked-previous-issues 17 | attributes: 18 | label: "Have you spent some time to check if this issue has been raised before?" 19 | description: "Please check if a similar issue has been raised before." 20 | options: 21 | - label: "I have googled for a similar issue or checked our older issues for a similar idea" 22 | required: true 23 | - type: checkboxes 24 | id: read-code-of-conduct 25 | attributes: 26 | label: "Have you read the [Code of Conduct](https://github.com/SoftCreatR/php-mistral-ai-sdk/blob/main/CODE_OF_CONDUCT.md)?" 27 | options: 28 | - label: "I have read the Code of Conduct" 29 | required: true 30 | - type: textarea 31 | id: pitch 32 | attributes: 33 | label: "Pitch" 34 | description: "Please explain why this feature should be implemented and how it would be used. Add examples, if applicable." 35 | placeholder: "Explain the reasoning behind the feature and provide examples..." 36 | validations: 37 | required: true 38 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 9 | 10 | # 🔀 Pull Request 11 | 12 | ## What does this PR do? 13 | 14 | (Provide a description of what this PR does.) 15 | 16 | ## Test Plan 17 | 18 | (Write your test plan here. If you changed any code, please provide us with clear instructions on how you verified your changes work.) 19 | 20 | ## Related PRs and Issues 21 | 22 | (If this PR is related to any other PR or resolves any issue or related to any issue link all related PR and issues here.) 23 | -------------------------------------------------------------------------------- /.github/workflows/create-release.yml: -------------------------------------------------------------------------------- 1 | name: Create Release 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | syntax-check: 10 | runs-on: ubuntu-latest 11 | if: startsWith(github.event.head_commit.message, '[Release]') 12 | 13 | strategy: 14 | matrix: 15 | php: [ '8.1', '8.2', '8.3', '8.4' ] 16 | 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v4 20 | 21 | - name: Setup PHP 22 | uses: shivammathur/setup-php@v2 23 | with: 24 | php-version: ${{ matrix.php }} 25 | 26 | - name: Install Composer dependencies 27 | run: composer install --no-dev 28 | 29 | - name: Check PHP syntax 30 | run: find {src,tests} -type f -name "*.php" -exec php -l {} \; 31 | 32 | analyze-code: 33 | needs: syntax-check 34 | runs-on: ubuntu-latest 35 | steps: 36 | - name: Checkout code 37 | uses: actions/checkout@v4 38 | 39 | - name: Setup PHP 40 | uses: shivammathur/setup-php@v2 41 | with: 42 | php-version: '8.1' 43 | tools: php-cs-fixer, phpstan 44 | extensions: mbstring 45 | 46 | - name: Install Composer dependencies 47 | run: composer install 48 | 49 | - name: Run PHP-CS-Fixer 50 | run: php-cs-fixer fix --dry-run --diff 51 | 52 | - name: Run PHPStan 53 | run: phpstan analyse src tests 54 | 55 | check-dependencies: 56 | needs: analyze-code 57 | runs-on: ubuntu-latest 58 | 59 | steps: 60 | - name: Checkout code 61 | uses: actions/checkout@v4 62 | 63 | - name: Setup PHP 64 | uses: shivammathur/setup-php@v2 65 | with: 66 | php-version: '8.1' 67 | 68 | - name: Install Composer dependencies 69 | run: composer install 70 | 71 | - name: Run security checker 72 | run: composer require --dev enlightn/security-checker && vendor/bin/security-checker security:check 73 | 74 | run-tests: 75 | needs: check-dependencies 76 | runs-on: ubuntu-latest 77 | 78 | steps: 79 | - name: Checkout code 80 | uses: actions/checkout@v4 81 | 82 | - name: Setup PHP 83 | uses: shivammathur/setup-php@v2 84 | with: 85 | php-version: '8.1' 86 | coverage: pcov 87 | ini-values: zend.assertions=1 88 | 89 | - name: Install Composer dependencies 90 | run: composer install 91 | 92 | - name: Run PHPUnit 93 | run: vendor/bin/phpunit --coverage-clover=coverage.xml 94 | 95 | - name: Upload coverage report to Codecov 96 | uses: codecov/codecov-action@v3 97 | with: 98 | token: ${{ secrets.CODECOV_TOKEN }} 99 | file: coverage.xml 100 | fail_ci_if_error: true 101 | 102 | - name: Upload coverage report to Code Climate 103 | uses: paambaati/codeclimate-action@v5 104 | env: 105 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 106 | with: 107 | coverageLocations: ${{github.workspace}}/coverage.xml:clover 108 | 109 | create-release: 110 | needs: run-tests 111 | runs-on: ubuntu-latest 112 | 113 | steps: 114 | - name: Checkout code 115 | uses: actions/checkout@v4 116 | 117 | - name: Setup PHP 118 | uses: shivammathur/setup-php@v2 119 | with: 120 | php-version: '8.1' 121 | 122 | - name: Install Composer dependencies 123 | run: composer install --no-dev 124 | 125 | - name: Get current version 126 | id: current-version 127 | run: | 128 | VERSION=$(composer config version --quiet) 129 | echo "Current version: $VERSION" 130 | echo "::set-output name=version::$VERSION" 131 | 132 | - name: Create Release 133 | id: create-release 134 | uses: actions/create-release@v1 135 | env: 136 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 137 | with: 138 | tag_name: v${{ steps.current-version.outputs.version }} 139 | release_name: Release v${{ steps.current-version.outputs.version }} 140 | body: 'New release for version ${{ steps.current-version.outputs.version }}' 141 | draft: false 142 | prerelease: false 143 | -------------------------------------------------------------------------------- /.github/workflows/update-contributors.yml: -------------------------------------------------------------------------------- 1 | name: Update Contributors 2 | 3 | on: 4 | pull_request: 5 | types: [closed] 6 | branches: 7 | - main 8 | if: github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'main' 9 | workflow_dispatch: 10 | 11 | jobs: 12 | update-contributors: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: write 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v4 20 | 21 | - name: Add contributors 22 | uses: BobAnkh/add-contributors@v0.2.2 23 | with: 24 | CONTRIBUTOR: '## Contributors ✨' 25 | ACCESS_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | IGNORED_CONTRIBUTORS: 'Sascha Greuel' 27 | -------------------------------------------------------------------------------- /.github/workflows/validate-pr.yml: -------------------------------------------------------------------------------- 1 | name: Validate PR 2 | 3 | on: 4 | pull_request: 5 | paths: 6 | - 'composer.json' 7 | - '**.php' 8 | 9 | jobs: 10 | syntax-check: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | php: [ '8.1', '8.2', '8.3', '8.4' ] 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v4 18 | 19 | - name: Setup PHP 20 | uses: shivammathur/setup-php@v2 21 | with: 22 | php-version: ${{ matrix.php }} 23 | 24 | - name: Install Composer dependencies 25 | run: composer install --no-dev 26 | 27 | - name: Check PHP syntax 28 | run: find {src,tests} -type f -name "*.php" -exec php -l {} \; 29 | 30 | analyze-code: 31 | needs: syntax-check 32 | runs-on: ubuntu-latest 33 | steps: 34 | - name: Checkout code 35 | uses: actions/checkout@v4 36 | 37 | - name: Setup PHP 38 | uses: shivammathur/setup-php@v2 39 | with: 40 | php-version: '8.1' 41 | tools: php-cs-fixer, phpstan 42 | extensions: mbstring 43 | 44 | - name: Install Composer dependencies 45 | run: composer install 46 | 47 | - name: Run PHP-CS-Fixer 48 | run: php-cs-fixer fix --dry-run --diff 49 | 50 | - name: Run PHPStan 51 | run: phpstan analyse src tests 52 | 53 | check-dependencies: 54 | needs: analyze-code 55 | runs-on: ubuntu-latest 56 | 57 | steps: 58 | - name: Checkout code 59 | uses: actions/checkout@v4 60 | 61 | - name: Setup PHP 62 | uses: shivammathur/setup-php@v2 63 | with: 64 | php-version: '8.1' 65 | 66 | - name: Install Composer dependencies 67 | run: composer install 68 | 69 | - name: Run security checker 70 | run: composer require --dev enlightn/security-checker && vendor/bin/security-checker security:check 71 | 72 | run-tests: 73 | needs: check-dependencies 74 | runs-on: ubuntu-latest 75 | 76 | steps: 77 | - name: Checkout code 78 | uses: actions/checkout@v4 79 | 80 | - name: Setup PHP 81 | uses: shivammathur/setup-php@v2 82 | with: 83 | php-version: '8.1' 84 | coverage: pcov 85 | ini-values: zend.assertions=1 86 | 87 | - name: Install Composer dependencies 88 | run: composer install 89 | 90 | - name: Run PHPUnit 91 | run: vendor/bin/phpunit --coverage-clover=coverage.xml 92 | 93 | - name: Upload coverage report to Codecov 94 | uses: codecov/codecov-action@v3 95 | with: 96 | token: ${{ secrets.CODECOV_TOKEN }} 97 | file: coverage.xml 98 | fail_ci_if_error: true 99 | 100 | - name: Upload coverage report to Code Climate 101 | uses: paambaati/codeclimate-action@v5 102 | env: 103 | CC_TEST_REPORTER_ID: ${{ secrets.CC_TEST_REPORTER_ID }} 104 | with: 105 | coverageLocations: ${{github.workspace}}/coverage.xml:clover 106 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /composer.lock 2 | vendor/ 3 | /*.cache 4 | .idea/ 5 | /coverage.xml 6 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | exclude('*/vendor/*') 4 | ->exclude('node_modules') 5 | ->in(__DIR__) 6 | ->notPath('lib/system/api'); 7 | 8 | return (new PhpCsFixer\Config()) 9 | ->setRiskyAllowed(true) 10 | ->setRules([ 11 | '@PSR1' => true, 12 | '@PSR2' => true, 13 | '@PSR12' => true, 14 | '@PER-CS' => true, 15 | 16 | 'array_push' => true, 17 | 'backtick_to_shell_exec' => true, 18 | 'no_alias_language_construct_call' => true, 19 | 'no_mixed_echo_print' => true, 20 | 'pow_to_exponentiation' => true, 21 | 'random_api_migration' => true, 22 | 23 | 'array_syntax' => ['syntax' => 'short'], 24 | 'no_multiline_whitespace_around_double_arrow' => true, 25 | 'no_trailing_comma_in_singleline' => true, 26 | 'no_whitespace_before_comma_in_array' => true, 27 | 'normalize_index_brace' => true, 28 | 'whitespace_after_comma_in_array' => true, 29 | 30 | 'non_printable_character' => ['use_escape_sequences_in_strings' => true], 31 | 32 | 'magic_constant_casing' => true, 33 | 'magic_method_casing' => true, 34 | 'native_function_casing' => true, 35 | 'native_type_declaration_casing' => true, 36 | 37 | 'cast_spaces' => ['space' => 'none'], 38 | 'no_unset_cast' => true, 39 | 40 | 'class_attributes_separation' => true, 41 | 'no_null_property_initialization' => true, 42 | 'self_accessor' => true, 43 | 'single_class_element_per_statement' => true, 44 | 45 | 'no_empty_comment' => true, 46 | 'single_line_comment_style' => ['comment_types' => ['hash']], 47 | 48 | 'native_constant_invocation' => ['strict' => false], 49 | 50 | 'no_alternative_syntax' => true, 51 | 'no_trailing_comma_in_singleline' => true, 52 | 'no_unneeded_control_parentheses' => ['statements' => ['break', 'clone', 'continue', 'echo_print', 'return', 'switch_case', 'yield', 'yield_from']], 53 | 'no_unneeded_braces' => ['namespaces' => true], 54 | 'switch_continue_to_break' => true, 55 | 'trailing_comma_in_multiline' => ['elements' => ['arrays']], 56 | 57 | 'type_declaration_spaces' => true, 58 | 'lambda_not_used_import' => true, 59 | 'native_function_invocation' => ['include' => ['@internal']], 60 | 'no_unreachable_default_argument_value' => true, 61 | 'nullable_type_declaration_for_default_null_value' => true, 62 | 'static_lambda' => true, 63 | 64 | 'fully_qualified_strict_types' => true, 65 | 'no_unused_imports' => true, 66 | 67 | 'dir_constant' => true, 68 | 'explicit_indirect_variable' => true, 69 | 'function_to_constant' => true, 70 | 'is_null' => true, 71 | 'no_unset_on_property' => true, 72 | 73 | 'list_syntax' => ['syntax' => 'short'], 74 | 75 | 'clean_namespace' => true, 76 | 'no_leading_namespace_whitespace' => true, 77 | 78 | 'no_homoglyph_names' => true, 79 | 80 | 'binary_operator_spaces' => true, 81 | 'concat_space' => ['spacing' => 'one'], 82 | 'increment_style' => ['style' => 'post'], 83 | 'logical_operators' => true, 84 | 'object_operator_without_whitespace' => true, 85 | 'operator_linebreak' => true, 86 | 'standardize_increment' => true, 87 | 'standardize_not_equals' => true, 88 | 'ternary_to_elvis_operator' => true, 89 | 'ternary_to_null_coalescing' => true, 90 | 'unary_operator_spaces' => true, 91 | 92 | 'no_useless_return' => true, 93 | 'return_assignment' => true, 94 | 95 | 'multiline_whitespace_before_semicolons' => true, 96 | 'no_empty_statement' => true, 97 | 'no_singleline_whitespace_before_semicolons' => true, 98 | 'space_after_semicolon' => ['remove_in_empty_for_expressions' => true], 99 | 100 | 'escape_implicit_backslashes' => true, 101 | 'explicit_string_variable' => true, 102 | 'heredoc_to_nowdoc' => true, 103 | 'no_binary_string' => true, 104 | 'simple_to_complex_string_variable' => true, 105 | 106 | 'array_indentation' => true, 107 | 'blank_line_before_statement' => ['statements' => ['return', 'exit']], 108 | 'method_chaining_indentation' => true, 109 | 'no_extra_blank_lines' => ['tokens' => ['case', 'continue', 'curly_brace_block', 'default', 'extra', 'parenthesis_brace_block', 'square_brace_block', 'switch', 'throw', 'use']], 110 | 'no_spaces_around_offset' => true, 111 | 112 | // SoftCreatR style 113 | 'global_namespace_import' => [ 114 | 'import_classes' => true, 115 | 'import_constants' => true, 116 | 'import_functions' => false, 117 | ], 118 | 'ordered_imports' => [ 119 | 'imports_order' => ['class', 'function', 'const'], 120 | ], 121 | ]) 122 | ->setFinder($finder); 123 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [2.0.0] - 2024-10-06 9 | 10 | ### Removed 11 | 12 | - Dropped support for PHP 7.4. **PHP 8.1 or higher is now required**. 13 | 14 | ### Added 15 | 16 | - **Streaming Support**: 17 | - Added support for streaming responses in the `MistralAI` class, allowing real-time token generation for the `createChatCompletion` method and other applicable endpoints. 18 | - Implemented a callback mechanism for handling streamed data in real time. 19 | 20 | - **New Endpoints**: 21 | - **Models**: 22 | - `retrieveModel`: Retrieve information about a specific model by its ID. 23 | - `deleteModel`: Delete a fine-tuned model by its ID. 24 | - `updateFineTunedModel`: Update fine-tuning for a specific model. 25 | - `archiveModel`: Archive a fine-tuned model. 26 | - `unarchiveModel`: Unarchive a fine-tuned model. 27 | - **Files**: 28 | - `uploadFile`: Upload a file for use in fine-tuning. 29 | - `listFiles`: Retrieve a list of all uploaded files. 30 | - `retrieveFile`: Retrieve details of a specific file by its ID. 31 | - `deleteFile`: Delete a file by its ID. 32 | - **Fine-Tuning Jobs**: 33 | - `listFineTuningJobs`: Get a list of all fine-tuning jobs. 34 | - `retrieveFineTuningJob`: Retrieve details of a specific fine-tuning job by its ID. 35 | - `cancelFineTuningJob`: Cancel a fine-tuning job. 36 | - `startFineTuningJob`: Start a fine-tuning job. 37 | - `createFineTuningJob`: Create a new fine-tuning job. 38 | - **FIM Completion**: 39 | - `createFimCompletion`: Generate Fill-In-the-Middle (FIM) text completions. 40 | - **Agents Completion**: 41 | - `createAgentsCompletion`: Generate completions based on agents or specialized workflows. 42 | 43 | - **New Examples**: 44 | - Created new example files to showcase API usage and functionality. All examples were aligned with the OpenAPI specification, using relevant model descriptions and proper documentation: 45 | - **Chat Completion with Streaming**: `examples/chat/createChatCompletion.php` 46 | - Demonstrates how to create a chat completion with the `mistral-small-latest` model, featuring real-time response streaming. 47 | - **Retrieve Model**: `examples/models/retrieveModel.php` 48 | - Demonstrates how to retrieve detailed information about a specific model using the `retrieveModel` endpoint. 49 | - **Delete Model**: `examples/models/deleteModel.php` 50 | - Shows how to delete a specific model using the `deleteModel` endpoint. 51 | - **Archive/Unarchive Model**: 52 | - `examples/models/archiveModel.php` 53 | - `examples/models/unarchiveModel.php` 54 | - These examples show how to archive and unarchive models respectively. 55 | - **File Management**: 56 | - **Upload File**: `examples/files/uploadFile.php` 57 | - **List Files**: `examples/files/listFiles.php` 58 | - **Retrieve File**: `examples/files/retrieveFile.php` 59 | - **Delete File**: `examples/files/deleteFile.php` 60 | - **Fine-Tuning Jobs**: 61 | - **List Fine-Tuning Jobs**: `examples/finetuning/listFineTuningJobs.php` 62 | - **Retrieve Fine-Tuning Job**: `examples/finetuning/retrieveFineTuningJob.php` 63 | - **Cancel Fine-Tuning Job**: `examples/finetuning/cancelFineTuningJob.php` 64 | - **Start Fine-Tuning Job**: `examples/finetuning/startFineTuningJob.php` 65 | - **Create Fine-Tuning Job**: `examples/finetuning/createFineTuningJob.php` 66 | - **FIM Completion**: `examples/fim/createFimCompletion.php` 67 | - Demonstrates the `createFimCompletion` method to generate Fill-In-the-Middle completions. 68 | - **Agents Completion**: `examples/agents/createAgentsCompletion.php` 69 | - Shows how to use the `createAgentsCompletion` method to generate agents-based text completions. 70 | 71 | - **Factory Updates**: 72 | - Added real-time processing of streamed content in the `MistralAIFactory::request` method. 73 | 74 | - **Enhanced Documentation**: 75 | - Provided detailed docblocks for all example files. 76 | - Example docblocks include model descriptions, usage instructions, expected output, and references to the OpenAPI specification. 77 | 78 | ### Changed 79 | 80 | - **Refactor of the `MistralAI` class**: 81 | - Adjusted the `__call` method to better align with the updated `MistralAIURLBuilder`, ensuring proper extraction of parameters and options. 82 | - Enhanced error handling and validation for parameter inputs to improve robustness. 83 | - Updated request generation to handle both JSON and multipart request bodies, as required by specific API endpoints (e.g., file uploads). 84 | - Implemented better multipart handling for file upload scenarios. 85 | - **Refactor of the Unit Tests**: 86 | - Updated unit tests to reflect the new API endpoints and streaming capabilities. 87 | - Added new test cases for the new endpoints and streaming functionality. 88 | 89 | 90 | ## [1.0.0] - 2024-02-03 91 | 92 | ### Added 93 | 94 | - Initial release of the Mistral AI PHP library. 95 | - Basic implementation for making API calls to the Mistral API. 96 | - Unit tests for the initial implementation. 97 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | ## Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to make participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | * Using welcoming and inclusive language 12 | * Being respectful of differing viewpoints and experiences 13 | * Gracefully accepting constructive criticism 14 | * Focusing on what is best for the community 15 | * Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | * Trolling, insulting/derogatory comments, and personal or political attacks 21 | * Public or private harassment 22 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | * Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project maintainer at . All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project maintainer is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), version 1.4, available at [https://www.contributor-covenant.org/version/1/4/code-of-conduct.html](https://www.contributor-covenant.org/version/1/4/code-of-conduct.html) 44 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2024, Sascha Greuel and Contributors 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, 4 | provided that the above copyright notice and this permission notice appear in all copies. 5 | 6 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL 7 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 8 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 9 | ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF 10 | THIS SOFTWARE. 11 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mistral API Wrapper for PHP 2 | 3 | [![Build](https://img.shields.io/github/actions/workflow/status/SoftCreatR/php-mistral-ai-sdk/.github/workflows/create-release.yml?branch=main)](https://github.com/SoftCreatR/php-mistral-ai-sdk/actions/workflows/create-release.yml) [![Latest Release](https://img.shields.io/packagist/v/SoftCreatR/php-mistral-ai-sdk?color=blue&label=Latest%20Release)](https://packagist.org/packages/softcreatr/php-mistral-ai-sdk) [![ISC licensed](https://img.shields.io/badge/license-ISC-blue.svg)](./LICENSE.md) [![Plant Tree](https://img.shields.io/badge/dynamic/json?color=brightgreen&label=Plant%20Tree&query=%24.total&url=https%3A%2F%2Fpublic.ecologi.com%2Fusers%2Fsoftcreatr%2Ftrees)](https://ecologi.com/softcreatr?r=61212ab3fc69b8eb8a2014f4) [![Codecov branch](https://img.shields.io/codecov/c/github/SoftCreatR/php-mistral-ai-sdk)](https://codecov.io/gh/SoftCreatR/php-mistral-ai-sdk) [![Code Climate maintainability](https://img.shields.io/codeclimate/maintainability-percentage/SoftCreatR/php-mistral-ai-sdk)](https://codeclimate.com/github/SoftCreatR/php-mistral-ai-sdk) 4 | 5 | This PHP library provides a simple wrapper for the Mistral API, allowing you to easily integrate the Mistral API into your PHP projects. 6 | 7 | ## Features 8 | 9 | - Easy integration with Mistral API 10 | - Supports all Mistral API endpoints 11 | - Streaming support for real-time responses in chat completions 12 | - Utilizes PSR-17 and PSR-18 compliant HTTP clients and factories for making API requests 13 | 14 | ## Requirements 15 | 16 | - PHP 8.1 or higher 17 | - A PSR-17 HTTP Factory implementation (e.g., [guzzle/psr7](https://github.com/guzzle/psr7) or [nyholm/psr7](https://github.com/Nyholm/psr7)) 18 | - A PSR-18 HTTP Client implementation (e.g., [guzzlehttp/guzzle](https://github.com/guzzle/guzzle) or [symfony/http-client](https://github.com/symfony/http-client)) 19 | 20 | ## Installation 21 | 22 | You can install the library via [Composer](https://getcomposer.org/): 23 | 24 | ```bash 25 | composer require softcreatr/php-mistral-ai-sdk 26 | ``` 27 | 28 | ## Usage 29 | 30 | First, include the library in your project: 31 | 32 | ```php 33 | createChatCompletion([ 58 | 'model' => 'mistral-tiny', 59 | 'messages' => [ 60 | [ 61 | 'role' => 'user', 62 | 'content' => 'Who is the most renowned French painter?' 63 | ], 64 | ], 65 | ]); 66 | 67 | // Process the API response 68 | if ($response->getStatusCode() === 200) { 69 | $responseObj = json_decode($response->getBody()->getContents(), true); 70 | 71 | print_r($responseObj); 72 | } else { 73 | echo "Error: " . $response->getStatusCode(); 74 | } 75 | ``` 76 | 77 | ### Streaming Example 78 | 79 | You can enable real-time streaming for chat completions: 80 | 81 | ```php 82 | $streamCallback = static function ($data) { 83 | if (isset($data['choices'][0]['delta']['content'])) { 84 | echo $data['choices'][0]['delta']['content']; 85 | } 86 | }; 87 | 88 | $mistral->createChatCompletion([ 89 | 'model' => 'mistral-small-latest', 90 | 'messages' => [ 91 | [ 92 | 'role' => 'user', 93 | 'content' => 'Tell me a story about a brave knight.', 94 | ], 95 | ], 96 | 'stream' => true, 97 | ], $streamCallback); 98 | ``` 99 | 100 | For more details on how to use each endpoint, refer to the [Mistral API documentation](https://docs.mistral.ai), and the [examples](https://github.com/SoftCreatR/php-mistral-ai-sdk/tree/main/examples) provided in the repository. 101 | 102 | ## Supported Methods 103 | 104 | ### Chat Completions 105 | - [Create Chat Completion](https://docs.mistral.ai/api/#tag/chat/operation/chat_completion_v1_chat_completions_post) - [Example](https://github.com/SoftCreatR/php-mistral-ai-sdk/blob/main/examples/chat/createChatCompletion.php) 106 | - `createChatCompletion(array $options = [], callable $streamCallback = null)` 107 | 108 | ### Embeddings 109 | - [Create Embedding](https://docs.mistral.ai/api/#tag/embeddings/operation/embeddings_v1_embeddings_post) - [Example](https://github.com/SoftCreatR/php-mistral-ai-sdk/blob/main/examples/embeddings/createEmbedding.php) 110 | - `createEmbedding(array $options = [])` 111 | 112 | ### Models 113 | - [List Models](https://docs.mistral.ai/api/#tag/models/operation/list_models_v1_models_get) - [Example](https://github.com/SoftCreatR/php-mistral-ai-sdk/blob/main/examples/models/listModels.php) 114 | - `listModels()` 115 | - [Retrieve Model](https://docs.mistral.ai/api/#tag/models/operation/retrieve_model_v1_models__model_id__get) - [Example](https://github.com/SoftCreatR/php-mistral-ai-sdk/blob/main/examples/models/retrieveModel.php) 116 | - `retrieveModel(array $parameters = [])` 117 | - [Delete Model](https://docs.mistral.ai/api/#tag/models/operation/delete_model_v1_models__model_id__delete) - [Example](https://github.com/SoftCreatR/php-mistral-ai-sdk/blob/main/examples/models/deleteModel.php) 118 | - `deleteModel(array $parameters = [])` 119 | - [Update Fine-Tuned Model](https://docs.mistral.ai/api/#tag/models/operation/jobs_api_routes_fine_tuning_update_fine_tuned_model) - [Example](https://github.com/SoftCreatR/php-mistral-ai-sdk/blob/main/examples/models/updateFineTunedModel.php) 120 | - `updateFineTunedModel(array $parameters = [])` 121 | - [Archive Model](https://docs.mistral.ai/api/#tag/models/operation/jobs_api_routes_fine_tuning_archive_fine_tuned_model) - [Example](https://github.com/SoftCreatR/php-mistral-ai-sdk/blob/main/examples/models/archiveModel.php) 122 | - `archiveModel(array $parameters = [])` 123 | - [Unarchive Model](https://docs.mistral.ai/api/#tag/models/operation/jobs_api_routes_fine_tuning_unarchive_fine_tuned_model) - [Example](https://github.com/SoftCreatR/php-mistral-ai-sdk/blob/main/examples/models/unarchiveModel.php) 124 | - `unarchiveModel(array $parameters = [])` 125 | 126 | ### Files 127 | - [Upload File](https://docs.mistral.ai/api/#tag/files/operation/files_api_routes_upload_file) - [Example](https://github.com/SoftCreatR/php-mistral-ai-sdk/blob/main/examples/files/uploadFile.php) 128 | - `uploadFile(array $options = [])` 129 | - [List Files](https://docs.mistral.ai/api/#tag/files/operation/files_api_routes_list_files) - [Example](https://github.com/SoftCreatR/php-mistral-ai-sdk/blob/main/examples/files/listFiles.php) 130 | - `listFiles()` 131 | - [Retrieve File](https://docs.mistral.ai/api/#tag/files/operation/files_api_routes_retrieve_file) - [Example](https://github.com/SoftCreatR/php-mistral-ai-sdk/blob/main/examples/files/retrieveFile.php) 132 | - `retrieveFile(array $parameters = [])` 133 | - [Delete File](https://docs.mistral.ai/api/#tag/files/operation/files_api_routes_delete_file) - [Example](https://github.com/SoftCreatR/php-mistral-ai-sdk/blob/main/examples/files/deleteFile.php) 134 | - `deleteFile(array $parameters = [])` 135 | 136 | ### Fine-Tuning Jobs 137 | - [List Fine-Tuning Jobs](https://docs.mistral.ai/api/#tag/fine-tuning/operation/jobs_api_routes_fine_tuning_get_fine_tuning_jobs) - [Example](https://github.com/SoftCreatR/php-mistral-ai-sdk/blob/main/examples/finetuning/listFineTuningJobs.php) 138 | - `listFineTuningJobs()` 139 | - [Retrieve Fine-Tuning Job](https://docs.mistral.ai/api/#tag/fine-tuning/operation/jobs_api_routes_fine_tuning_get_fine_tuning_job) - [Example](https://github.com/SoftCreatR/php-mistral-ai-sdk/blob/main/examples/finetuning/retrieveFineTuningJob.php) 140 | - `retrieveFineTuningJob(array $parameters = [])` 141 | - [Cancel Fine-Tuning Job](https://docs.mistral.ai/api/#tag/fine-tuning/operation/jobs_api_routes_fine_tuning_cancel_fine_tuning_job) - [Example](https://github.com/SoftCreatR/php-mistral-ai-sdk/blob/main/examples/finetuning/cancelFineTuningJob.php) 142 | - `cancelFineTuningJob(array $parameters = [])` 143 | - [Start Fine-Tuning Job](https://docs.mistral.ai/api/#tag/fine-tuning/operation/jobs_api_routes_fine_tuning_start_fine_tuning_job) - [Example](https://github.com/SoftCreatR/php-mistral-ai-sdk/blob/main/examples/finetuning/startFineTuningJob.php) 144 | - `startFineTuningJob(array $parameters = [])` 145 | - [Create Fine-Tuning Job](https://docs.mistral.ai/api/#tag/fine-tuning/operation/jobs_api_routes_fine_tuning_create_fine_tuning_job) - [Example](https://github.com/SoftCreatR/php-mistral-ai-sdk/blob/main/examples/finetuning/createFineTuningJob.php) 146 | - `createFineTuningJob(array $options = [])` 147 | 148 | ### FIM Completion 149 | - [Create FIM Completion](https://docs.mistral.ai/api/#tag/fim/operation/fim_completion_v1_fim_completions_post) - [Example](https://github.com/SoftCreatR/php-mistral-ai-sdk/blob/main/examples/fim/createFimCompletion.php) 150 | - `createFimCompletion(array $options = [])` 151 | 152 | ### Agents Completion 153 | - [Create Agents Completion](https://docs.mistral.ai/api/#tag/agents/operation/agents_completion_v1_agents_completions_post) - [Example](https://github.com/SoftCreatR/php-mistral-ai-sdk/blob/main/examples/agents/createAgentsCompletion.php) 154 | - `createAgentsCompletion(array $options = [])` 155 | 156 | ## Changelog 157 | 158 | For a detailed list of changes and updates, please refer to the [CHANGELOG.md](https://github.com/SoftCreatR/php-mistral-ai-sdk/blob/main/CHANGELOG.md) file. We adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html) and document notable changes for each release. 159 | 160 | ## Known Problems and Limitations 161 | 162 | ### Streaming Support 163 | Streaming is now supported for real-time token generation in chat completions. Please make sure you are handling streams correctly using a callback, as demonstrated in the examples. 164 | 165 | ## License 166 | 167 | This library is licensed under the ISC License. See the [LICENSE](https://github.com/SoftCreatR/php-mistral-ai-sdk/blob/main/LICENSE.md) file for more information. 168 | 169 | ## Maintainers 🛠️ 170 | 171 | 172 | 173 | 180 | 181 |
174 | 175 | Sascha Greuel 176 |
177 | Sascha Greuel 178 |
179 |
182 | 183 | ## Contributors ✨ 184 | 185 | 186 | 187 | 188 |
189 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "softcreatr/php-mistral-ai-sdk", 3 | "description": "A powerful and easy-to-use PHP SDK for the Mistral AI API, allowing seamless integration of advanced AI-powered features into your PHP projects.", 4 | "license": "ISC", 5 | "type": "library", 6 | "version": "2.0.0", 7 | "keywords": [ 8 | "mistral", 9 | "ai", 10 | "llama", 11 | "replit", 12 | "gpt", 13 | "gpt-3", 14 | "gpt-4", 15 | "artificial-intelligence", 16 | "machine-learning", 17 | "natural-language-processing", 18 | "nlp", 19 | "php", 20 | "sdk", 21 | "api-wrapper" 22 | ], 23 | "authors": [ 24 | { 25 | "name": "Sascha Greuel", 26 | "email": "hello@1-2.dev" 27 | } 28 | ], 29 | "require": { 30 | "php": ">=8.1", 31 | "ext-json": "*", 32 | "psr/http-client": "^1.0", 33 | "psr/http-factory": "^1.1", 34 | "psr/http-message": "^2.0" 35 | }, 36 | "require-dev": { 37 | "friendsofphp/php-cs-fixer": "^3", 38 | "guzzlehttp/guzzle": "^7", 39 | "phpunit/phpunit": "^10" 40 | }, 41 | "minimum-stability": "stable", 42 | "autoload": { 43 | "psr-4": { 44 | "SoftCreatR\\MistralAI\\": "src/" 45 | } 46 | }, 47 | "autoload-dev": { 48 | "psr-4": { 49 | "SoftCreatR\\MistralAI\\Tests\\": "tests/" 50 | } 51 | }, 52 | "config": { 53 | "sort-packages": true 54 | }, 55 | "scripts": { 56 | "phpcsf": "php-cs-fixer fix", 57 | "test": "phpunit" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /examples/MistralAIFactory.php: -------------------------------------------------------------------------------- 1 | true, 62 | ]); 63 | 64 | return new MistralAI( 65 | requestFactory: $psr17Factory, 66 | streamFactory: $psr17Factory, 67 | uriFactory: $psr17Factory, 68 | httpClient: $httpClient, 69 | apiKey: $apiKey 70 | ); 71 | } 72 | 73 | /** 74 | * Sends a request to the specified MistralAI API endpoint. 75 | * 76 | * @param string $method The name of the API method to call. 77 | * @param array $parameters An associative array of parameters (URL parameters or request options). 78 | * @param callable|null $streamCallback Optional callback function for streaming responses. 79 | * 80 | * @return void 81 | */ 82 | public static function request(string $method, array $parameters = [], ?callable $streamCallback = null): void 83 | { 84 | $mistralAI = self::create(); 85 | 86 | try { 87 | $endpoint = MistralAIURLBuilder::getEndpoint($method); 88 | $path = $endpoint['path']; 89 | 90 | // Determine if the path contains placeholders 91 | $hasPlaceholders = \preg_match('/\{(\w+)}/', $path) === 1; 92 | 93 | if ($hasPlaceholders) { 94 | $urlParameters = $parameters; 95 | $bodyOptions = []; 96 | } else { 97 | $urlParameters = []; 98 | $bodyOptions = $parameters; 99 | } 100 | 101 | if ($streamCallback !== null) { 102 | $mistralAI->{$method}($urlParameters, $bodyOptions, $streamCallback); 103 | } else { 104 | $response = $mistralAI->{$method}($urlParameters, $bodyOptions); 105 | 106 | $result = \json_decode( 107 | $response->getBody()->getContents(), 108 | true, 109 | 512, 110 | \JSON_THROW_ON_ERROR 111 | ); 112 | 113 | echo "============\n| Response |\n============\n\n"; 114 | echo \json_encode($result, \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT); 115 | echo "\n\n============\n"; 116 | } 117 | } catch (Exception $e) { 118 | echo "Error: {$e->getMessage()}\n"; 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /examples/agents/createAgentsCompletion.php: -------------------------------------------------------------------------------- 1 | 'agent-1234', // Replace with your actual agent ID 34 | 'messages' => [ 35 | [ 36 | 'role' => 'user', 37 | 'content' => 'Tell me a joke about cats.', 38 | ], 39 | ], 40 | ]); 41 | -------------------------------------------------------------------------------- /examples/chat/createChatCompletion.php: -------------------------------------------------------------------------------- 1 | 'mistral-small-latest', 35 | 'messages' => [ 36 | [ 37 | 'role' => 'user', 38 | 'content' => 'Who is the best French painter? Answer in one short sentence.', 39 | ], 40 | ], 41 | 'temperature' => 0.7, 42 | 'top_p' => 1, 43 | ]); 44 | -------------------------------------------------------------------------------- /examples/chat/createChatCompletionStream.php: -------------------------------------------------------------------------------- 1 | 'mistral-small-latest', 35 | 'messages' => [ 36 | [ 37 | 'role' => 'user', 38 | 'content' => 'Tell me a story about a brave knight.', 39 | ], 40 | ], 41 | 'stream' => true, 42 | ], static function ($data) { 43 | if (isset($data['choices'][0]['delta']['content'])) { 44 | echo $data['choices'][0]['delta']['content']; 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /examples/embeddings/createEmbedding.php: -------------------------------------------------------------------------------- 1 | 'mistral-embed', 34 | 'input' => 'The quick brown fox jumps over the lazy dog.', 35 | ]); 36 | -------------------------------------------------------------------------------- /examples/files/deleteFile.php: -------------------------------------------------------------------------------- 1 | 'file-abc123']); 33 | -------------------------------------------------------------------------------- /examples/files/listFiles.php: -------------------------------------------------------------------------------- 1 | 'file-abc123']); 33 | -------------------------------------------------------------------------------- /examples/files/uploadFile.php: -------------------------------------------------------------------------------- 1 | '/path/to/your/training_data.jsonl', // Replace with the actual file path 34 | 'purpose' => 'fine-tune', 35 | ]); 36 | -------------------------------------------------------------------------------- /examples/fim/createFimCompletion.php: -------------------------------------------------------------------------------- 1 | 'codestral-2405', 36 | 'prompt' => 'def', 37 | 'suffix' => 'return a + b', 38 | 'temperature' => 0.7, 39 | 'top_p' => 1, 40 | ]); 41 | -------------------------------------------------------------------------------- /examples/fine_tuning/cancelFineTuningJob.php: -------------------------------------------------------------------------------- 1 | 'job-abc123']); 33 | -------------------------------------------------------------------------------- /examples/fine_tuning/createFineTuningJob.php: -------------------------------------------------------------------------------- 1 | 'open-mistral-7b', 35 | 'training_files' => [ 36 | [ 37 | 'file_id' => 'file-abc123', // Replace with your actual training file ID 38 | 'weight' => 1, 39 | ], 40 | ], 41 | 'hyperparameters' => [ 42 | 'learning_rate' => 0.0001, 43 | 'epochs' => 4, 44 | ], 45 | 'suffix' => 'my-fine-tuned-model', 46 | ]); 47 | -------------------------------------------------------------------------------- /examples/fine_tuning/listFineTuningJobs.php: -------------------------------------------------------------------------------- 1 | 'job-abc123']); 33 | -------------------------------------------------------------------------------- /examples/fine_tuning/startFineTuningJob.php: -------------------------------------------------------------------------------- 1 | 'job-abc123']); 33 | -------------------------------------------------------------------------------- /examples/models/archiveModel.php: -------------------------------------------------------------------------------- 1 | 'ft:open-mistral-7b:my-great-model:abc123']); 33 | -------------------------------------------------------------------------------- /examples/models/deleteModel.php: -------------------------------------------------------------------------------- 1 | 'ft:open-mistral-7b:my-great-model:abc123']); 33 | -------------------------------------------------------------------------------- /examples/models/listModels.php: -------------------------------------------------------------------------------- 1 | 'mistral-small-latest']); 34 | -------------------------------------------------------------------------------- /examples/models/unarchiveModel.php: -------------------------------------------------------------------------------- 1 | 'ft:open-mistral-7b:my-great-model:abc123']); 33 | -------------------------------------------------------------------------------- /examples/models/updateFineTunedModel.php: -------------------------------------------------------------------------------- 1 | 'ft:open-mistral-7b:my-great-model:abc123'], [ 33 | 'name' => 'Updated Fine-Tuned Model Name', 34 | 'description' => 'This is an updated description for the fine-tuned model.', 35 | ]); 36 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 15 | 16 | 17 | tests 18 | 19 | 20 | 21 | 22 | 23 | src 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Exception/MistralAIException.php: -------------------------------------------------------------------------------- 1 | extractErrorMessageFromJson($message), $code, $previous); 50 | } 51 | 52 | /** 53 | * Attempts to extract the error message from a JSON-encoded string. 54 | * 55 | * If the provided error message is a JSON string containing an "error.message", 56 | * this method will extract and return that message. Otherwise, it returns the original message. 57 | * 58 | * @param string $errorMessage The error message, potentially encoded as a JSON string. 59 | * 60 | * @return string The extracted error message, or the original message if extraction fails. 61 | */ 62 | private function extractErrorMessageFromJson(string $errorMessage): string 63 | { 64 | try { 65 | $decoded = \json_decode($errorMessage, true, 512, JSON_THROW_ON_ERROR); 66 | 67 | if (isset($decoded['error']['message']) && \is_string($decoded['error']['message'])) { 68 | return $decoded['error']['message']; 69 | } 70 | } catch (JsonException) { 71 | // Ignore JSON decoding errors and return the original message 72 | } 73 | 74 | return $errorMessage; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/MistralAI.php: -------------------------------------------------------------------------------- 1 | extractCallArguments($args); 102 | 103 | return $this->callAPI($httpMethod, $key, $parameters, $opts, $streamCallback); 104 | } 105 | 106 | /** 107 | * Extracts the arguments from the input array. 108 | * 109 | * @param array $args The input arguments. 110 | * 111 | * @return array An array containing the extracted parameters, options, and stream callback. 112 | * 113 | * @throws InvalidArgumentException If the first argument is not an array. 114 | */ 115 | private function extractCallArguments(array $args): array 116 | { 117 | $parameters = []; 118 | $opts = []; 119 | $streamCallback = null; 120 | 121 | if (!isset($args[0])) { 122 | return [$parameters, $opts, $streamCallback]; 123 | } 124 | 125 | if (\is_array($args[0])) { 126 | $parameters = $args[0]; 127 | 128 | if (isset($args[1]) && \is_array($args[1])) { 129 | $opts = $args[1]; 130 | 131 | if (isset($args[2]) && \is_callable($args[2])) { 132 | $streamCallback = $args[2]; 133 | } 134 | } elseif (isset($args[1]) && \is_callable($args[1])) { 135 | $streamCallback = $args[1]; 136 | } 137 | } else { 138 | throw new InvalidArgumentException('First argument must be an array of parameters.'); 139 | } 140 | 141 | return [$parameters, $opts, $streamCallback]; 142 | } 143 | 144 | /** 145 | * Calls the MistralAI API with the provided method, key, parameters, and options. 146 | * 147 | * @param string $method The HTTP method for the request. 148 | * @param string $key The API endpoint key. 149 | * @param array $parameters Parameters for URL placeholders. 150 | * @param array $opts The options for the request body or query. 151 | * @param callable|null $streamCallback Callback function to handle streaming data. 152 | * 153 | * @return ResponseInterface|null The API response or null if streaming. 154 | * 155 | * @throws MistralAIException If the API returns an error. 156 | * @throws RandomException 157 | */ 158 | private function callAPI( 159 | string $method, 160 | string $key, 161 | array $parameters = [], 162 | array $opts = [], 163 | ?callable $streamCallback = null 164 | ): ?ResponseInterface { 165 | $uri = MistralAIURLBuilder::createUrl( 166 | $this->uriFactory, 167 | $key, 168 | $parameters, 169 | $this->origin, 170 | $this->apiVersion 171 | ); 172 | 173 | return $this->sendRequest($uri, $method, $opts, $streamCallback); 174 | } 175 | 176 | /** 177 | * Sends an HTTP request to the MistralAI API and returns the response. 178 | * 179 | * @param UriInterface $uri The URI to send the request to. 180 | * @param string $method The HTTP method to use. 181 | * @param array $params Parameters to include in the request body. 182 | * @param callable|null $streamCallback Callback function to handle streaming data. 183 | * 184 | * @return ResponseInterface|null The response from the MistralAI API or null if streaming. 185 | * 186 | * @throws MistralAIException If the API returns an error. 187 | * @throws RandomException 188 | */ 189 | private function sendRequest( 190 | UriInterface $uri, 191 | string $method, 192 | array $params = [], 193 | ?callable $streamCallback = null 194 | ): ?ResponseInterface { 195 | $request = $this->requestFactory->createRequest($method, $uri); 196 | $isMultipart = $this->isMultipartRequest($params); 197 | $boundary = $isMultipart ? $this->generateMultipartBoundary() : null; 198 | $headers = $this->createHeaders($isMultipart, $boundary); 199 | $request = $this->applyHeaders($request, $headers); 200 | 201 | $body = $isMultipart 202 | ? $this->createMultipartStream($params, $boundary) 203 | : $this->createJsonBody($params); 204 | 205 | if ($body !== '') { 206 | $request = $request->withBody($this->streamFactory->createStream($body)); 207 | } 208 | 209 | try { 210 | if ($streamCallback !== null && ($params['stream'] ?? false) === true) { 211 | $this->handleStreamingResponse($request, $streamCallback); 212 | 213 | return null; 214 | } 215 | 216 | $response = $this->httpClient->sendRequest($request); 217 | 218 | if ($response->getStatusCode() >= 400) { 219 | throw new MistralAIException($response->getBody()->getContents(), $response->getStatusCode()); 220 | } 221 | 222 | return $response; 223 | } catch (ClientExceptionInterface $e) { 224 | throw new MistralAIException($e->getMessage(), $e->getCode(), $e); 225 | } 226 | } 227 | 228 | /** 229 | * Handles a streaming response from the API. 230 | * 231 | * @param RequestInterface $request The request to send. 232 | * @param callable $streamCallback The callback function to handle streaming data. 233 | * 234 | * @return void 235 | * 236 | * @throws MistralAIException If an error occurs during streaming. 237 | */ 238 | private function handleStreamingResponse(RequestInterface $request, callable $streamCallback): void 239 | { 240 | try { 241 | $response = $this->httpClient->sendRequest($request); 242 | $statusCode = $response->getStatusCode(); 243 | 244 | if ($statusCode >= 400) { 245 | throw new MistralAIException($response->getBody()->getContents(), $statusCode); 246 | } 247 | 248 | $body = $response->getBody(); 249 | $buffer = ''; 250 | 251 | while (!$body->eof()) { 252 | $chunk = $body->read(8192); 253 | $buffer .= $chunk; 254 | 255 | while (($newlinePos = \strpos($buffer, "\n")) !== false) { 256 | $line = \substr($buffer, 0, $newlinePos); 257 | $buffer = \substr($buffer, $newlinePos + 1); 258 | 259 | $data = \trim($line); 260 | 261 | if ($data === '') { 262 | continue; 263 | } 264 | 265 | if ($data === 'data: [DONE]') { 266 | return; 267 | } 268 | 269 | if (\str_starts_with($data, 'data: ')) { 270 | $json = \substr($data, 6); 271 | 272 | try { 273 | $decoded = \json_decode($json, true, 512, JSON_THROW_ON_ERROR); 274 | $streamCallback($decoded); 275 | } catch (JsonException $e) { 276 | throw new MistralAIException('JSON decode error: ' . $e->getMessage(), 0, $e); 277 | } 278 | } 279 | } 280 | } 281 | } catch (ClientExceptionInterface $e) { 282 | throw new MistralAIException($e->getMessage(), $e->getCode(), $e); 283 | } 284 | } 285 | 286 | /** 287 | * Generates a unique multipart boundary string. 288 | * 289 | * @return string The generated multipart boundary string. 290 | * 291 | * @throws RandomException 292 | */ 293 | private function generateMultipartBoundary(): string 294 | { 295 | return '----MistralAI' . \bin2hex(\random_bytes(16)); 296 | } 297 | 298 | /** 299 | * Creates the headers for an API request. 300 | * 301 | * @param bool $isMultipart Indicates whether the request is multipart. 302 | * @param string|null $boundary The multipart boundary string, if applicable. 303 | * 304 | * @return array An associative array of headers. 305 | */ 306 | private function createHeaders(bool $isMultipart, ?string $boundary): array 307 | { 308 | return [ 309 | 'Authorization' => 'Bearer ' . $this->apiKey, 310 | 'Content-Type' => $isMultipart 311 | ? "multipart/form-data; boundary={$boundary}" 312 | : 'application/json', 313 | ]; 314 | } 315 | 316 | /** 317 | * Applies the headers to the given request. 318 | * 319 | * @param RequestInterface $request The request to apply headers to. 320 | * @param array $headers An associative array of headers to apply. 321 | * 322 | * @return RequestInterface The request with headers applied. 323 | */ 324 | private function applyHeaders(RequestInterface $request, array $headers): RequestInterface 325 | { 326 | foreach ($headers as $key => $value) { 327 | $request = $request->withHeader($key, $value); 328 | } 329 | 330 | return $request; 331 | } 332 | 333 | /** 334 | * Creates a JSON-encoded body string from the given parameters. 335 | * 336 | * @param array $params An associative array of parameters to encode as JSON. 337 | * 338 | * @return string The JSON-encoded body string. 339 | * 340 | * @throws MistralAIException If JSON encoding fails. 341 | */ 342 | private function createJsonBody(array $params): string 343 | { 344 | if (empty($params)) { 345 | return ''; 346 | } 347 | 348 | try { 349 | return \json_encode($params, JSON_THROW_ON_ERROR); 350 | } catch (JsonException $e) { 351 | throw new MistralAIException('JSON encode error: ' . $e->getMessage(), 0, $e); 352 | } 353 | } 354 | 355 | /** 356 | * Creates a multipart stream for sending files in a request. 357 | * 358 | * @param array $params An associative array of parameters to send with the request. 359 | * @param string $boundary A string used as a boundary to separate parts of the multipart stream. 360 | * 361 | * @return string The multipart stream as a string. 362 | */ 363 | private function createMultipartStream(array $params, string $boundary): string 364 | { 365 | $multipartStream = ''; 366 | 367 | foreach ($params as $key => $value) { 368 | $multipartStream .= "--{$boundary}\r\n"; 369 | $multipartStream .= "Content-Disposition: form-data; name=\"{$key}\""; 370 | 371 | if ($key === 'file' && \is_string($value) && \file_exists($value)) { 372 | $filename = \basename($value); 373 | $fileContents = \file_get_contents($value); 374 | $multipartStream .= "; filename=\"{$filename}\"\r\n"; 375 | $multipartStream .= "Content-Type: application/octet-stream\r\n\r\n"; 376 | $multipartStream .= "{$fileContents}\r\n"; 377 | } else { 378 | $multipartStream .= "\r\n\r\n{$value}\r\n"; 379 | } 380 | } 381 | 382 | $multipartStream .= "--{$boundary}--\r\n"; 383 | 384 | return $multipartStream; 385 | } 386 | 387 | /** 388 | * Determines if a request is a multipart request based on the provided parameters. 389 | * 390 | * @param array $params An associative array of parameters to check. 391 | * 392 | * @return bool True if the request is a multipart request, false otherwise. 393 | */ 394 | private function isMultipartRequest(array $params): bool 395 | { 396 | return isset($params['file']); 397 | } 398 | } 399 | -------------------------------------------------------------------------------- /src/MistralAIURLBuilder.php: -------------------------------------------------------------------------------- 1 | 46 | */ 47 | private static array $urlEndpoints = [ 48 | // Models 49 | 'listModels' => ['method' => self::HTTP_METHOD_GET, 'path' => '/models'], 50 | 'retrieveModel' => ['method' => self::HTTP_METHOD_GET, 'path' => '/models/{model_id}'], 51 | 'deleteModel' => ['method' => self::HTTP_METHOD_DELETE, 'path' => '/models/{model_id}'], 52 | 'updateFineTunedModel' => ['method' => self::HTTP_METHOD_PATCH, 'path' => '/fine_tuning/models/{model_id}'], 53 | 'archiveModel' => ['method' => self::HTTP_METHOD_POST, 'path' => '/fine_tuning/models/{model_id}/archive'], 54 | 'unarchiveModel' => ['method' => self::HTTP_METHOD_DELETE, 'path' => '/fine_tuning/models/{model_id}/archive'], 55 | 56 | // Files 57 | 'uploadFile' => ['method' => self::HTTP_METHOD_POST, 'path' => '/files'], 58 | 'listFiles' => ['method' => self::HTTP_METHOD_GET, 'path' => '/files'], 59 | 'retrieveFile' => ['method' => self::HTTP_METHOD_GET, 'path' => '/files/{file_id}'], 60 | 'deleteFile' => ['method' => self::HTTP_METHOD_DELETE, 'path' => '/files/{file_id}'], 61 | 62 | // Fine-Tuning Jobs 63 | 'listFineTuningJobs' => ['method' => self::HTTP_METHOD_GET, 'path' => '/fine_tuning/jobs'], 64 | 'retrieveFineTuningJob' => ['method' => self::HTTP_METHOD_GET, 'path' => '/fine_tuning/jobs/{job_id}'], 65 | 'cancelFineTuningJob' => ['method' => self::HTTP_METHOD_POST, 'path' => '/fine_tuning/jobs/{job_id}/cancel'], 66 | 'startFineTuningJob' => ['method' => self::HTTP_METHOD_POST, 'path' => '/fine_tuning/jobs/{job_id}/start'], 67 | 'createFineTuningJob' => ['method' => self::HTTP_METHOD_POST, 'path' => '/fine_tuning/jobs'], 68 | 69 | // Chat Completion 70 | 'createChatCompletion' => ['method' => self::HTTP_METHOD_POST, 'path' => '/chat/completions'], 71 | 72 | // FIM Completion 73 | 'createFimCompletion' => ['method' => self::HTTP_METHOD_POST, 'path' => '/fim/completions'], 74 | 75 | // Agents Completion 76 | 'createAgentsCompletion' => ['method' => self::HTTP_METHOD_POST, 'path' => '/agents/completions'], 77 | 78 | // Embeddings 79 | 'createEmbedding' => ['method' => self::HTTP_METHOD_POST, 'path' => '/embeddings'], 80 | ]; 81 | 82 | /** 83 | * Prevents instantiation of this class. 84 | */ 85 | private function __construct() 86 | { 87 | // This class should not be instantiated. 88 | } 89 | 90 | /** 91 | * Gets the MistralAI API endpoint configuration. 92 | * 93 | * @param string $key The endpoint key. 94 | * 95 | * @return array{method: string, path: string} The endpoint configuration. 96 | * 97 | * @throws InvalidArgumentException If the provided key is invalid. 98 | */ 99 | public static function getEndpoint(string $key): array 100 | { 101 | if (!isset(self::$urlEndpoints[$key])) { 102 | throw new InvalidArgumentException(\sprintf('Invalid Mistral AI URL key "%s".', $key)); 103 | } 104 | 105 | return self::$urlEndpoints[$key]; 106 | } 107 | 108 | /** 109 | * Creates a URL for the specified MistralAI API endpoint. 110 | * 111 | * @param UriFactoryInterface $uriFactory The PSR-17 URI factory instance used for creating URIs. 112 | * @param string $key The key representing the API endpoint. 113 | * @param array $parameters Optional parameters to replace in the endpoint path. 114 | * @param string $origin Custom origin (hostname), if needed. 115 | * @param string $apiVersion Custom API version, if different from the default. 116 | * 117 | * @return UriInterface The fully constructed URL for the API endpoint. 118 | * 119 | * @throws InvalidArgumentException If a required path parameter is missing or invalid. 120 | */ 121 | public static function createUrl( 122 | UriFactoryInterface $uriFactory, 123 | string $key, 124 | array $parameters = [], 125 | string $origin = '', 126 | string $apiVersion = '' 127 | ): UriInterface { 128 | $endpoint = self::getEndpoint($key); 129 | $path = self::replacePathParameters($endpoint['path'], $parameters); 130 | 131 | return $uriFactory 132 | ->createUri() 133 | ->withScheme('https') 134 | ->withHost($origin !== '' ? $origin : self::ORIGIN) 135 | ->withPath('/' . ($apiVersion !== '' ? $apiVersion : self::API_VERSION) . $path); 136 | } 137 | 138 | /** 139 | * Replaces path parameters in the given path with provided parameter values. 140 | * 141 | * @param string $path The path containing parameter placeholders. 142 | * @param array $parameters The parameter values to replace placeholders in the path. 143 | * 144 | * @return string The path with replaced parameter values. 145 | * 146 | * @throws InvalidArgumentException If a required path parameter is missing or invalid. 147 | */ 148 | private static function replacePathParameters(string $path, array $parameters): string 149 | { 150 | return \preg_replace_callback('/\{(\w+)}/', static function ($matches) use ($parameters) { 151 | $key = $matches[1]; 152 | 153 | if (!\array_key_exists($key, $parameters)) { 154 | throw new InvalidArgumentException(\sprintf('Missing path parameter "%s".', $key)); 155 | } 156 | 157 | $value = $parameters[$key]; 158 | 159 | if (!\is_scalar($value)) { 160 | throw new InvalidArgumentException(\sprintf( 161 | 'Parameter "%s" must be a scalar value, %s given.', 162 | $key, 163 | \gettype($value) 164 | )); 165 | } 166 | 167 | return (string)$value; 168 | }, $path); 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /tests/MistralAITest.php: -------------------------------------------------------------------------------- 1 | mockedClient = $this->createMock(ClientInterface::class); 72 | 73 | $this->mistralAI = new MistralAI( 74 | $psr17Factory, 75 | $psr17Factory, 76 | $psr17Factory, 77 | $this->mockedClient, 78 | $this->apiKey, 79 | $this->origin 80 | ); 81 | } 82 | 83 | /** 84 | * Tests that an InvalidArgumentException is thrown when the first argument is not an array. 85 | */ 86 | public function testInvalidFirstArgumentInCall(): void 87 | { 88 | $this->expectException(InvalidArgumentException::class); 89 | $this->expectExceptionMessage('First argument must be an array of parameters.'); 90 | 91 | /** @noinspection PhpParamsInspection */ 92 | $this->mistralAI->createChatCompletion('invalid_argument'); 93 | } 94 | 95 | /** 96 | * Tests that the createMultipartStream method is called and the boundary is generated. 97 | * 98 | * @throws Exception 99 | */ 100 | public function testUploadFileCreatesMultipartStream(): void 101 | { 102 | $filePath = __DIR__ . '/fixtures/dummyFile.jsonl'; 103 | \file_put_contents($filePath, 'Dummy content'); 104 | 105 | $this->sendRequestMock(function (RequestInterface $request) use ($filePath) { 106 | $body = (string)$request->getBody(); 107 | $this->assertStringContainsString('multipart/form-data', $request->getHeaderLine('Content-Type')); 108 | $this->assertStringContainsString('Dummy content', $body); 109 | $this->assertStringContainsString(\basename($filePath), $body); 110 | 111 | return new Response(200, [], '{"success": true}'); 112 | }); 113 | 114 | // Pass parameters as $opts, not $parameters 115 | $response = $this->mistralAI->uploadFile([], [ 116 | 'file' => $filePath, 117 | 'purpose' => 'fine-tune', 118 | ]); 119 | 120 | $this->assertEquals(200, $response->getStatusCode()); 121 | 122 | \unlink($filePath); 123 | } 124 | 125 | /** 126 | * Tests that a MistralAIException is thrown when the API returns an error response. 127 | */ 128 | public function testCallAPIHandlesErrorResponse(): void 129 | { 130 | $this->sendRequestMock(static function () { 131 | return new Response(400, [], 'Bad Request'); 132 | }); 133 | 134 | $this->expectException(MistralAIException::class); 135 | $this->expectExceptionMessage('Bad Request'); 136 | 137 | $this->mistralAI->createChatCompletion([ 138 | 'model' => 'mistral-tiny', 139 | 'messages' => [ 140 | ['role' => 'user', 'content' => 'Test message'], 141 | ], 142 | ]); 143 | } 144 | 145 | /** 146 | * Tests that a MistralAIException is thrown when the HTTP client throws a ClientExceptionInterface. 147 | */ 148 | public function testCallAPICatchesClientException(): void 149 | { 150 | $this->sendRequestMock(static function () { 151 | throw new class ('Client error', 0) extends Exception implements ClientExceptionInterface {}; 152 | }); 153 | 154 | $this->expectException(MistralAIException::class); 155 | $this->expectExceptionMessage('Client error'); 156 | 157 | $this->mistralAI->createChatCompletion([ 158 | 'model' => 'mistral-tiny', 159 | 'messages' => [ 160 | ['role' => 'user', 'content' => 'Test message'], 161 | ], 162 | ]); 163 | } 164 | 165 | /** 166 | * Tests that handleStreamingResponse throws a MistralAIException when the response status code is >= 400. 167 | */ 168 | public function testHandleStreamingResponseHandlesErrorResponse(): void 169 | { 170 | $this->sendRequestMock(static function () { 171 | return new Response(400, [], 'Bad Request'); 172 | }); 173 | 174 | $this->expectException(MistralAIException::class); 175 | $this->expectExceptionMessage('Bad Request'); 176 | 177 | $this->mistralAI->createChatCompletion([ 178 | 'model' => 'mistral-tiny', 179 | 'messages' => [ 180 | ['role' => 'user', 'content' => 'Test message'], 181 | ], 182 | 'stream' => true, 183 | ]); 184 | } 185 | 186 | /** 187 | * Tests that handleStreamingResponse continues when data is an empty string. 188 | */ 189 | public function testHandleStreamingResponseContinuesOnEmptyData(): void 190 | { 191 | $fakeResponseContent = "\n"; // Empty data 192 | $stream = \fopen('php://temp', 'rb+'); 193 | \fwrite($stream, $fakeResponseContent); 194 | \rewind($stream); 195 | 196 | $fakeResponse = new Response(200, [], $stream); 197 | 198 | $this->sendRequestMock(static function () use ($fakeResponse) { 199 | return $fakeResponse; 200 | }); 201 | 202 | $this->mistralAI->createChatCompletion( 203 | [], 204 | [ 205 | 'model' => 'mistral-tiny', 206 | 'messages' => [ 207 | ['role' => 'user', 'content' => 'Test message'], 208 | ], 209 | 'stream' => true, 210 | ], 211 | function () { 212 | $this->fail('Streaming callback should not be called on empty data.'); 213 | } 214 | ); 215 | 216 | $this->assertTrue(true); // If no exception is thrown, test passes 217 | } 218 | 219 | /** 220 | * Tests that handleStreamingResponse throws a MistralAIException when JSON decoding fails. 221 | */ 222 | public function testHandleStreamingResponseJsonException(): void 223 | { 224 | $fakeResponseContent = "data: invalid_json\n"; 225 | $stream = \fopen('php://temp', 'rb+'); 226 | \fwrite($stream, $fakeResponseContent); 227 | \rewind($stream); 228 | 229 | $fakeResponse = new Response(200, [], $stream); 230 | 231 | $this->sendRequestMock(static function () use ($fakeResponse) { 232 | return $fakeResponse; 233 | }); 234 | 235 | $this->expectException(MistralAIException::class); 236 | $this->expectExceptionMessage('JSON decode error: Syntax error'); 237 | 238 | // Correctly pass parameters and options 239 | $this->mistralAI->createChatCompletion( 240 | [], 241 | [ 242 | 'model' => 'mistral-tiny', 243 | 'messages' => [ 244 | ['role' => 'user', 'content' => 'Test message'], 245 | ], 246 | 'stream' => true, 247 | ], 248 | static function ($data) { 249 | // Streaming callback 250 | } 251 | ); 252 | } 253 | 254 | /** 255 | * Tests that handleStreamingResponse catches ClientExceptionInterface exceptions. 256 | */ 257 | public function testHandleStreamingResponseCatchesClientException(): void 258 | { 259 | $this->sendRequestMock(static function () { 260 | throw new class ('Client error in streaming', 0) extends Exception implements ClientExceptionInterface {}; 261 | }); 262 | 263 | $this->expectException(MistralAIException::class); 264 | $this->expectExceptionMessage('Client error in streaming'); 265 | 266 | $this->mistralAI->createChatCompletion([ 267 | 'model' => 'mistral-tiny', 268 | 'messages' => [ 269 | ['role' => 'user', 'content' => 'Test message'], 270 | ], 271 | 'stream' => true, 272 | ]); 273 | } 274 | 275 | /** 276 | * Tests that generateMultipartBoundary generates a boundary string. 277 | * 278 | * @throws ReflectionException 279 | */ 280 | public function testGenerateMultipartBoundary(): void 281 | { 282 | $reflectionMethod = TestHelper::getPrivateMethod($this->mistralAI, 'generateMultipartBoundary'); 283 | $boundary = $reflectionMethod->invoke($this->mistralAI); 284 | 285 | $this->assertMatchesRegularExpression('/^----MistralAI[0-9a-f]{32}$/', $boundary); 286 | } 287 | 288 | /** 289 | * Tests that createHeaders sets the correct Content-Type for multipart requests. 290 | * 291 | * @throws ReflectionException 292 | */ 293 | public function testCreateHeadersForMultipartRequest(): void 294 | { 295 | $reflectionMethod = TestHelper::getPrivateMethod($this->mistralAI, 'createHeaders'); 296 | $boundary = 'testBoundary'; 297 | 298 | $headers = $reflectionMethod->invoke($this->mistralAI, true, $boundary); 299 | 300 | $this->assertArrayHasKey('Content-Type', $headers); 301 | $this->assertEquals("multipart/form-data; boundary={$boundary}", $headers['Content-Type']); 302 | } 303 | 304 | /** 305 | * Tests that createJsonBody throws a MistralAIException when JSON encoding fails. 306 | * 307 | * @throws ReflectionException 308 | */ 309 | public function testCreateJsonBodyJsonException(): void 310 | { 311 | $reflectionMethod = TestHelper::getPrivateMethod($this->mistralAI, 'createJsonBody'); 312 | 313 | $this->expectException(MistralAIException::class); 314 | 315 | // Since exception messages can vary, you can omit the exact message or adjust it to match. 316 | $this->expectExceptionMessageMatches('/^JSON encode error:/'); 317 | 318 | $invalidValue = \tmpfile(); // Cannot be JSON encoded 319 | $params = ['invalid' => $invalidValue]; 320 | 321 | $reflectionMethod->invoke($this->mistralAI, $params); 322 | } 323 | 324 | /** 325 | * Tests that createMultipartStream creates a valid multipart stream. 326 | * 327 | * @throws ReflectionException 328 | */ 329 | public function testCreateMultipartStream(): void 330 | { 331 | $reflectionMethod = TestHelper::getPrivateMethod($this->mistralAI, 'createMultipartStream'); 332 | $boundary = 'testBoundary'; 333 | $filePath = __DIR__ . '/fixtures/dummyFile.jsonl'; 334 | \file_put_contents($filePath, 'Dummy content'); 335 | 336 | $params = [ 337 | 'file' => $filePath, 338 | 'purpose' => 'fine-tune', 339 | ]; 340 | 341 | $multipartStream = $reflectionMethod->invoke($this->mistralAI, $params, $boundary); 342 | 343 | $this->assertStringContainsString("--{$boundary}\r\n", $multipartStream); 344 | $this->assertStringContainsString('Content-Disposition: form-data; name="file"; filename="dummyFile.jsonl"', $multipartStream); 345 | $this->assertStringContainsString('Dummy content', $multipartStream); 346 | 347 | \unlink($filePath); 348 | } 349 | 350 | /** 351 | * Tests that the createChatCompletion method handles API calls correctly. 352 | * 353 | * @throws Exception 354 | */ 355 | public function testCreateChatCompletion(): void 356 | { 357 | $this->testApiCall( 358 | fn() => $this->mistralAI->createChatCompletion([ 359 | 'model' => 'mistral-tiny', 360 | 'messages' => [ 361 | [ 362 | 'role' => 'user', 363 | 'content' => 'What is the best French cheese?', 364 | ], 365 | ], 366 | ]), 367 | 'chatCompletion.json' 368 | ); 369 | } 370 | 371 | /** 372 | * Tests that the createChatCompletion method handles streaming API calls correctly. 373 | * 374 | * @throws Exception 375 | */ 376 | public function testCreateChatCompletionWithStreaming(): void 377 | { 378 | $output = ''; 379 | 380 | $streamCallback = static function ($data) use (&$output) { 381 | if (isset($data['choices'][0]['delta']['content'])) { 382 | $output .= $data['choices'][0]['delta']['content']; 383 | } 384 | }; 385 | 386 | $this->testApiCallWithStreaming( 387 | fn($streamCallback) => $this->mistralAI->createChatCompletion( 388 | [], 389 | [ 390 | 'model' => 'mistral-small-latest', 391 | 'messages' => [ 392 | [ 393 | 'role' => 'user', 394 | 'content' => 'Tell me a story about a brave knight.', 395 | ], 396 | ], 397 | 'stream' => true, 398 | ], 399 | $streamCallback 400 | ), 401 | 'chatCompletionStreaming.json', 402 | $streamCallback 403 | ); 404 | 405 | $expectedOutput = 'Once upon a time, in a land far away, there lived a brave knight named Sir Alaric.'; 406 | $this->assertEquals($expectedOutput, $output); 407 | } 408 | 409 | /** 410 | * Tests that the createEmbedding method handles API calls correctly. 411 | * 412 | * @throws Exception 413 | */ 414 | public function testCreateEmbedding(): void 415 | { 416 | $this->testApiCall( 417 | fn() => $this->mistralAI->createEmbedding([ 418 | 'model' => 'mistral-embed', 419 | 'input' => ['Hello world', 'Test embedding'], 420 | ]), 421 | 'createEmbedding.json' 422 | ); 423 | } 424 | 425 | /** 426 | * Tests that the listModels method handles API calls correctly. 427 | * 428 | * @throws Exception 429 | */ 430 | public function testListModels(): void 431 | { 432 | $this->testApiCall( 433 | fn() => $this->mistralAI->listModels(), 434 | 'listModels.json' 435 | ); 436 | } 437 | 438 | /** 439 | * Tests that the retrieveModel method handles API calls correctly. 440 | * 441 | * @throws Exception 442 | */ 443 | public function testRetrieveModel(): void 444 | { 445 | $this->testApiCall( 446 | fn() => $this->mistralAI->retrieveModel(['model_id' => 'model_12345']), 447 | 'retrieveModel.json' 448 | ); 449 | } 450 | 451 | /** 452 | * Tests that the deleteModel method handles API calls correctly. 453 | * 454 | * @throws Exception 455 | */ 456 | public function testDeleteModel(): void 457 | { 458 | $this->testApiCall( 459 | fn() => $this->mistralAI->deleteModel(['model_id' => 'model_12345']), 460 | 'deleteModel.json' 461 | ); 462 | } 463 | 464 | /** 465 | * Tests that the updateFineTunedModel method handles API calls correctly. 466 | * 467 | * @throws Exception 468 | */ 469 | public function testUpdateFineTunedModel(): void 470 | { 471 | $this->testApiCall( 472 | fn() => $this->mistralAI->updateFineTunedModel([ 473 | 'model_id' => 'model_12345', 474 | 'new_parameter' => 'value', 475 | ]), 476 | 'updateFineTunedModel.json' 477 | ); 478 | } 479 | 480 | /** 481 | * Tests that the archiveModel method handles API calls correctly. 482 | * 483 | * @throws Exception 484 | */ 485 | public function testArchiveModel(): void 486 | { 487 | $this->testApiCall( 488 | fn() => $this->mistralAI->archiveModel(['model_id' => 'model_12345']), 489 | 'archiveModel.json' 490 | ); 491 | } 492 | 493 | /** 494 | * Tests that the unarchiveModel method handles API calls correctly. 495 | * 496 | * @throws Exception 497 | */ 498 | public function testUnarchiveModel(): void 499 | { 500 | $this->testApiCall( 501 | fn() => $this->mistralAI->unarchiveModel(['model_id' => 'model_12345']), 502 | 'unarchiveModel.json' 503 | ); 504 | } 505 | 506 | /** 507 | * Tests that the uploadFile method handles API calls correctly. 508 | * 509 | * @throws Exception 510 | */ 511 | public function testUploadFile(): void 512 | { 513 | $filePath = __DIR__ . '/fixtures/dummyFile.jsonl'; 514 | \file_put_contents($filePath, 'Dummy content'); 515 | 516 | $this->testApiCall( 517 | fn() => $this->mistralAI->uploadFile([ 518 | 'file' => $filePath, 519 | 'purpose' => 'fine-tune', 520 | ]), 521 | 'uploadFile.json' 522 | ); 523 | 524 | \unlink($filePath); 525 | } 526 | 527 | /** 528 | * Tests that the listFiles method handles API calls correctly. 529 | * 530 | * @throws Exception 531 | */ 532 | public function testListFiles(): void 533 | { 534 | $this->testApiCall( 535 | fn() => $this->mistralAI->listFiles(), 536 | 'listFiles.json' 537 | ); 538 | } 539 | 540 | /** 541 | * Tests that the retrieveFile method handles API calls correctly. 542 | * 543 | * @throws Exception 544 | */ 545 | public function testRetrieveFile(): void 546 | { 547 | $this->testApiCall( 548 | fn() => $this->mistralAI->retrieveFile(['file_id' => 'file_12345']), 549 | 'retrieveFile.json' 550 | ); 551 | } 552 | 553 | /** 554 | * Tests that the deleteFile method handles API calls correctly. 555 | * 556 | * @throws Exception 557 | */ 558 | public function testDeleteFile(): void 559 | { 560 | $this->testApiCall( 561 | fn() => $this->mistralAI->deleteFile(['file_id' => 'file_12345']), 562 | 'deleteFile.json' 563 | ); 564 | } 565 | 566 | /** 567 | * Tests that the listFineTuningJobs method handles API calls correctly. 568 | * 569 | * @throws Exception 570 | */ 571 | public function testListFineTuningJobs(): void 572 | { 573 | $this->testApiCall( 574 | fn() => $this->mistralAI->listFineTuningJobs(), 575 | 'listFineTuningJobs.json' 576 | ); 577 | } 578 | 579 | /** 580 | * Tests that the retrieveFineTuningJob method handles API calls correctly. 581 | * 582 | * @throws Exception 583 | */ 584 | public function testRetrieveFineTuningJob(): void 585 | { 586 | $this->testApiCall( 587 | fn() => $this->mistralAI->retrieveFineTuningJob(['job_id' => 'job_12345']), 588 | 'retrieveFineTuningJob.json' 589 | ); 590 | } 591 | 592 | /** 593 | * Tests that the cancelFineTuningJob method handles API calls correctly. 594 | * 595 | * @throws Exception 596 | */ 597 | public function testCancelFineTuningJob(): void 598 | { 599 | $this->testApiCall( 600 | fn() => $this->mistralAI->cancelFineTuningJob(['job_id' => 'job_12345']), 601 | 'cancelFineTuningJob.json' 602 | ); 603 | } 604 | 605 | /** 606 | * Tests that the startFineTuningJob method handles API calls correctly. 607 | * 608 | * @throws Exception 609 | */ 610 | public function testStartFineTuningJob(): void 611 | { 612 | $this->testApiCall( 613 | fn() => $this->mistralAI->startFineTuningJob(['job_id' => 'job_12345']), 614 | 'startFineTuningJob.json' 615 | ); 616 | } 617 | 618 | /** 619 | * Tests that the createFineTuningJob method handles API calls correctly. 620 | * 621 | * @throws Exception 622 | */ 623 | public function testCreateFineTuningJob(): void 624 | { 625 | $this->testApiCall( 626 | fn() => $this->mistralAI->createFineTuningJob([ 627 | 'training_file' => 'file_12345', 628 | 'model' => 'mistral-tiny', 629 | ]), 630 | 'createFineTuningJob.json' 631 | ); 632 | } 633 | 634 | /** 635 | * Tests that the createFimCompletion method handles API calls correctly. 636 | * 637 | * @throws Exception 638 | */ 639 | public function testCreateFimCompletion(): void 640 | { 641 | $this->testApiCall( 642 | fn() => $this->mistralAI->createFimCompletion([ 643 | 'model' => 'mistral-fim', 644 | 'prompt' => 'Once upon a time, in a land far away, there lived a brave knight named Sir Alaric.', 645 | 'insert_text' => 'Sir Alaric was known for his', 646 | ]), 647 | 'createFimCompletion.json' 648 | ); 649 | } 650 | 651 | /** 652 | * Tests that the createAgentsCompletion method handles API calls correctly. 653 | * 654 | * @throws Exception 655 | */ 656 | public function testCreateAgentsCompletion(): void 657 | { 658 | $this->testApiCall( 659 | fn() => $this->mistralAI->createAgentsCompletion([ 660 | 'model' => 'mistral-agent', 661 | 'tasks' => [ 662 | ['task' => 'Analyze sentiment', 'input' => 'I love programming!'], 663 | ['task' => 'Translate text', 'input' => 'Hello, how are you?', 'target_language' => 'es'], 664 | ], 665 | ]), 666 | 'createAgentsCompletion.json' 667 | ); 668 | } 669 | 670 | /** 671 | * Tests the 'extractCallArguments' method with various input scenarios. 672 | * 673 | * Ensures that the method correctly extracts parameters, options, and the stream callback from the provided arguments. 674 | * 675 | * @throws ReflectionException 676 | */ 677 | public function testExtractCallArguments(): void 678 | { 679 | $reflectionMethod = TestHelper::getPrivateMethod($this->mistralAI, 'extractCallArguments'); 680 | 681 | $testCases = [ 682 | // Parameters, Options, Stream Callback 683 | [ 684 | [['key' => 'value'], ['option_key' => 'option_value'], static function () {}], 685 | ['key' => 'value', 'option_key' => 'option_value', 'streamCallback' => true], 686 | ], 687 | // Parameters and Options without Stream Callback 688 | [ 689 | [['key' => 'value'], ['option_key' => 'option_value']], 690 | ['key' => 'value', 'option_key' => 'option_value', 'streamCallback' => null], 691 | ], 692 | // Only Parameters 693 | [ 694 | [['key' => 'value']], 695 | ['key' => 'value', 'option_key' => null, 'streamCallback' => null], 696 | ], 697 | // Empty array 698 | [ 699 | [], 700 | ['key' => null, 'option_key' => null, 'streamCallback' => null], 701 | ], 702 | ]; 703 | 704 | foreach ($testCases as $testCase) { 705 | [$args, $expected] = $testCase; 706 | $result = $reflectionMethod->invoke($this->mistralAI, $args); 707 | 708 | $this->assertIsArray($result); 709 | $this->assertCount(3, $result); 710 | 711 | // Validate parameters 712 | $this->assertEquals($expected['key'] ?? null, $result[0]['key'] ?? null); 713 | $this->assertEquals($expected['option_key'] ?? null, $result[1]['option_key'] ?? null); 714 | 715 | // Validate stream callback 716 | if ($expected['streamCallback'] === true) { 717 | $this->assertIsCallable($result[2]); 718 | } else { 719 | $this->assertNull($result[2]); 720 | } 721 | } 722 | } 723 | 724 | /** 725 | * Tests that callAPI handles JSON encoding errors correctly. 726 | * 727 | * Ensures that when JSON encoding fails due to an invalid value, 728 | * the method catches the JsonException and sets the request body to an empty string. 729 | */ 730 | public function testCallAPIJsonEncodingException(): void 731 | { 732 | $this->sendRequestMock(static function (RequestInterface $request) { 733 | $fakeResponse = new Response(200, [], ''); 734 | self::assertEquals('', (string)$request->getBody()); 735 | 736 | return $fakeResponse; 737 | }); 738 | 739 | $invalidValue = \tmpfile(); // Create an invalid value that cannot be JSON encoded 740 | $response = null; 741 | 742 | try { 743 | $response = $this->mistralAI->createChatCompletion([ 744 | 'model' => 'mistral-tiny', 745 | 'messages' => [ 746 | [ 747 | 'role' => 'system', 748 | 'content' => $invalidValue, 749 | ], 750 | ], 751 | ]); 752 | } catch (Exception) { 753 | // Exception is expected due to invalid parameter; ignore 754 | } 755 | 756 | self::assertNotNull($response, 'Response should not be null even if exception is caught.'); 757 | self::assertEquals(200, $response->getStatusCode()); 758 | self::assertEquals('', (string)$response->getBody()); 759 | } 760 | 761 | /** 762 | * Mocks an API call using a callable and a response file. 763 | * 764 | * Mocks the HTTP client to return a predefined response loaded from a file, 765 | * and checks if the status code and response body match the expected values. 766 | * 767 | * @param callable $apiCall The API call to test. 768 | * @param string $responseFile The path to the file containing the expected response. 769 | * 770 | * @throws Exception 771 | */ 772 | private function testApiCall(callable $apiCall, string $responseFile): void 773 | { 774 | $fakeResponseBody = TestHelper::loadResponseFromFile($responseFile); 775 | $fakeResponse = new Response(200, [], $fakeResponseBody); 776 | 777 | $this->sendRequestMock(static function () use ($fakeResponse) { 778 | return $fakeResponse; 779 | }); 780 | 781 | try { 782 | $response = $apiCall(); 783 | } catch (Exception) { 784 | $response = null; 785 | } 786 | 787 | self::assertNotNull($response, 'Response should not be null.'); 788 | self::assertEquals(200, $response->getStatusCode()); 789 | self::assertEquals($fakeResponseBody, (string)$response->getBody()); 790 | } 791 | 792 | /** 793 | * Mocks an API call with streaming support using a callable and a response file. 794 | * 795 | * Mocks the HTTP client to return a predefined streaming response loaded from a file, 796 | * and utilizes the provided stream callback to process the response. 797 | * 798 | * @param callable $apiCall The API call to test. 799 | * @param string $responseFile The path to the file containing the expected streaming response. 800 | * @param callable $streamCallback The callback function to handle streaming data. 801 | * 802 | * @throws Exception 803 | */ 804 | private function testApiCallWithStreaming(callable $apiCall, string $responseFile, callable $streamCallback): void 805 | { 806 | $fakeResponseContent = TestHelper::loadResponseFromFile($responseFile); 807 | $fakeChunks = \explode("\n", \trim($fakeResponseContent)); 808 | $stream = \fopen('php://temp', 'rb+'); 809 | 810 | foreach ($fakeChunks as $chunk) { 811 | \fwrite($stream, $chunk . "\n"); 812 | } 813 | \rewind($stream); 814 | 815 | $fakeResponse = new Response(200, [], $stream); 816 | 817 | $this->sendRequestMock(static function () use ($fakeResponse) { 818 | return $fakeResponse; 819 | }); 820 | 821 | try { 822 | $apiCall($streamCallback); 823 | } catch (Exception $e) { 824 | $this->fail('Exception occurred during streaming: ' . $e->getMessage()); 825 | } 826 | } 827 | 828 | /** 829 | * Sets up a mock for the sendRequest method of the mocked client. 830 | * 831 | * @param callable $responseCallback A callable that returns a response or throws an exception. 832 | */ 833 | private function sendRequestMock(callable $responseCallback): void 834 | { 835 | $this->mockedClient 836 | ->expects(self::once()) 837 | ->method('sendRequest') 838 | ->willReturnCallback($responseCallback); 839 | } 840 | } 841 | -------------------------------------------------------------------------------- /tests/MistralAIURLBuilderTest.php: -------------------------------------------------------------------------------- 1 | newInstanceWithoutConstructor(); 44 | 45 | // Invoke the constructor 46 | $constructor->invoke($instance); 47 | 48 | $this->assertInstanceOf(MistralAIURLBuilder::class, $instance); 49 | } 50 | 51 | /** 52 | * Tests that getEndpoint throws an exception for an invalid key. 53 | */ 54 | public function testGetEndpointWithInvalidKey(): void 55 | { 56 | $this->expectException(InvalidArgumentException::class); 57 | $this->expectExceptionMessage('Invalid Mistral AI URL key "invalidKey".'); 58 | 59 | MistralAIURLBuilder::getEndpoint('invalidKey'); 60 | } 61 | 62 | /** 63 | * Tests that createUrl throws an exception when a required path parameter is missing. 64 | */ 65 | public function testCreateUrlWithMissingPathParameter(): void 66 | { 67 | $this->expectException(InvalidArgumentException::class); 68 | $this->expectExceptionMessage('Missing path parameter "model_id".'); 69 | 70 | $uriFactory = new HttpFactory(); 71 | MistralAIURLBuilder::createUrl($uriFactory, 'retrieveModel', []); 72 | } 73 | 74 | /** 75 | * Tests that createUrl throws an exception when a path parameter is not scalar. 76 | */ 77 | public function testCreateUrlWithNonScalarPathParameter(): void 78 | { 79 | $this->expectException(InvalidArgumentException::class); 80 | $this->expectExceptionMessage('Parameter "model_id" must be a scalar value, array given.'); 81 | 82 | $uriFactory = new HttpFactory(); 83 | MistralAIURLBuilder::createUrl($uriFactory, 'retrieveModel', ['model_id' => ['not', 'scalar']]); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /tests/TestHelper.php: -------------------------------------------------------------------------------- 1 | getMethod($methodName); 41 | } 42 | 43 | /** 44 | * Retrieves the private constructor of a class using reflection. 45 | * 46 | * @param string $className The fully qualified class name. 47 | * 48 | * @return ReflectionMethod The reflection method instance representing the constructor. 49 | * 50 | * @throws ReflectionException If the class or constructor does not exist. 51 | */ 52 | public static function getPrivateConstructor(string $className): ReflectionMethod 53 | { 54 | $reflection = new ReflectionClass($className); 55 | $constructor = $reflection->getConstructor(); 56 | 57 | if ($constructor !== null) { 58 | return $constructor; 59 | } 60 | 61 | throw new ReflectionException("Constructor does not exist for class {$className}."); 62 | } 63 | 64 | /** 65 | * Loads the contents of a response file for testing purposes. 66 | * 67 | * @param string $filename The filename to load from the 'fixtures/responses' directory. 68 | * 69 | * @return string The contents of the response file. 70 | * 71 | * @throws RuntimeException If the file cannot be found or read. 72 | */ 73 | public static function loadResponseFromFile(string $filename): string 74 | { 75 | $filePath = __DIR__ . '/fixtures/responses/' . $filename; 76 | 77 | if (!\file_exists($filePath)) { 78 | throw new RuntimeException("Response file not found: {$filePath}"); 79 | } 80 | 81 | $content = \file_get_contents($filePath); 82 | 83 | if ($content === false) { 84 | throw new RuntimeException("Unable to read response file: {$filePath}"); 85 | } 86 | 87 | return $content; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/fixtures/responses/archiveModel.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "string", 3 | "object": "model", 4 | "archived": true 5 | } 6 | -------------------------------------------------------------------------------- /tests/fixtures/responses/cancelFineTuningJob.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", 3 | "auto_start": true, 4 | "hyperparameters": { 5 | "training_steps": 1, 6 | "learning_rate": 0.0001, 7 | "weight_decay": 0.1, 8 | "warmup_fraction": 0.05, 9 | "epochs": 0, 10 | "fim_ratio": 0.9 11 | }, 12 | "model": "open-mistral-7b", 13 | "status": "QUEUED", 14 | "job_type": "string", 15 | "created_at": 0, 16 | "modified_at": 0, 17 | "training_files": [ 18 | "497f6eca-6276-4993-bfeb-53cbbbba6f08" 19 | ], 20 | "validation_files": [], 21 | "object": "job", 22 | "fine_tuned_model": "string", 23 | "suffix": "string", 24 | "integrations": [ 25 | { 26 | "type": "wandb", 27 | "project": "string", 28 | "name": "string", 29 | "run_name": "string" 30 | } 31 | ], 32 | "trained_tokens": 0, 33 | "repositories": [], 34 | "metadata": { 35 | "expected_duration_seconds": 0, 36 | "cost": 0, 37 | "cost_currency": "string", 38 | "train_tokens_per_step": 0, 39 | "train_tokens": 0, 40 | "data_tokens": 0, 41 | "estimated_start_time": 0 42 | }, 43 | "events": [], 44 | "checkpoints": [] 45 | } 46 | -------------------------------------------------------------------------------- /tests/fixtures/responses/chatCompletion.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "cmpl-e5cc70bb28c444948073e77776eb30ef", 3 | "object": "chat.completion", 4 | "model": "mistral-small-latest", 5 | "usage": { 6 | "prompt_tokens": 16, 7 | "completion_tokens": 34, 8 | "total_tokens": 50 9 | }, 10 | "created": 1702256327, 11 | "choices": [ 12 | { 13 | "index": 0, 14 | "message": { 15 | "content": "string", 16 | "tool_calls": [ 17 | { 18 | "id": "null", 19 | "type": "function", 20 | "function": { 21 | "name": "string", 22 | "arguments": {} 23 | } 24 | } 25 | ], 26 | "prefix": false, 27 | "role": "assistant" 28 | }, 29 | "finish_reason": "stop" 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /tests/fixtures/responses/chatCompletionStreaming.json: -------------------------------------------------------------------------------- 1 | data: {"choices":[{"delta":{"content":"Once upon a time, "}}]} 2 | data: {"choices":[{"delta":{"content":"in a land far away, "}}]} 3 | data: {"choices":[{"delta":{"content":"there lived a brave knight named "}}]} 4 | data: {"choices":[{"delta":{"content":"Sir Alaric."}}]} 5 | data: [DONE] 6 | -------------------------------------------------------------------------------- /tests/fixtures/responses/createAgentsCompletion.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "cmpl-e5cc70bb28c444948073e77776eb30ef", 3 | "object": "chat.completion", 4 | "model": "mistral-small-latest", 5 | "usage": { 6 | "prompt_tokens": 16, 7 | "completion_tokens": 34, 8 | "total_tokens": 50 9 | }, 10 | "created": 1702256327, 11 | "choices": [ 12 | { 13 | "index": 0, 14 | "message": { 15 | "content": "string", 16 | "tool_calls": [ 17 | { 18 | "id": "null", 19 | "type": "function", 20 | "function": { 21 | "name": "string", 22 | "arguments": {} 23 | } 24 | } 25 | ], 26 | "prefix": false, 27 | "role": "assistant" 28 | }, 29 | "finish_reason": "stop" 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /tests/fixtures/responses/createEmbedding.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "cmpl-e5cc70bb28c444948073e77776eb30ef", 3 | "object": "chat.completion", 4 | "model": "mistral-small-latest", 5 | "usage": { 6 | "prompt_tokens": 16, 7 | "completion_tokens": 34, 8 | "total_tokens": 50 9 | }, 10 | "data": [ 11 | [ 12 | { 13 | "object": "embedding", 14 | "embedding": [ 15 | 0.1, 16 | 0.2, 17 | 0.3 18 | ], 19 | "index": 0 20 | }, 21 | { 22 | "object": "embedding", 23 | "embedding": [ 24 | 0.4, 25 | 0.5, 26 | 0.6 27 | ], 28 | "index": 1 29 | } 30 | ] 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /tests/fixtures/responses/createFimCompletion.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "cmpl-e5cc70bb28c444948073e77776eb30ef", 3 | "object": "chat.completion", 4 | "model": "codestral-latest", 5 | "usage": { 6 | "prompt_tokens": 16, 7 | "completion_tokens": 34, 8 | "total_tokens": 50 9 | }, 10 | "created": 1702256327, 11 | "choices": [ 12 | { 13 | "index": 0, 14 | "message": { 15 | "content": "string", 16 | "tool_calls": [ 17 | { 18 | "id": "null", 19 | "type": "function", 20 | "function": { 21 | "name": "string", 22 | "arguments": {} 23 | } 24 | } 25 | ], 26 | "prefix": false, 27 | "role": "assistant" 28 | }, 29 | "finish_reason": "stop" 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /tests/fixtures/responses/createFineTuningJob.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", 3 | "auto_start": true, 4 | "hyperparameters": { 5 | "training_steps": 1, 6 | "learning_rate": 0.0001, 7 | "weight_decay": 0.1, 8 | "warmup_fraction": 0.05, 9 | "epochs": 0, 10 | "fim_ratio": 0.9 11 | }, 12 | "model": "open-mistral-7b", 13 | "status": "QUEUED", 14 | "job_type": "string", 15 | "created_at": 0, 16 | "modified_at": 0, 17 | "training_files": [ 18 | "497f6eca-6276-4993-bfeb-53cbbbba6f08" 19 | ], 20 | "validation_files": [], 21 | "object": "job", 22 | "fine_tuned_model": "string", 23 | "suffix": "string", 24 | "integrations": [ 25 | { 26 | "type": "wandb", 27 | "project": "string", 28 | "name": "string", 29 | "run_name": "string" 30 | } 31 | ], 32 | "trained_tokens": 0, 33 | "repositories": [], 34 | "metadata": { 35 | "expected_duration_seconds": 0, 36 | "cost": 0, 37 | "cost_currency": "string", 38 | "train_tokens_per_step": 0, 39 | "train_tokens": 0, 40 | "data_tokens": 0, 41 | "estimated_start_time": 0 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /tests/fixtures/responses/deleteFile.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "497f6eca-6276-4993-bfeb-53cbbbba6f09", 3 | "object": "file", 4 | "deleted": false 5 | } 6 | -------------------------------------------------------------------------------- /tests/fixtures/responses/deleteModel.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "ft:open-mistral-7b:587a6b29:20240514:7e773925", 3 | "object": "model", 4 | "deleted": true 5 | } 6 | -------------------------------------------------------------------------------- /tests/fixtures/responses/listFiles.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [ 3 | { 4 | "id": "497f6eca-6276-4993-bfeb-53cbbbba6f09", 5 | "object": "file", 6 | "bytes": 13000, 7 | "created_at": 1716963433, 8 | "filename": "files_upload.jsonl", 9 | "purpose": "fine-tune", 10 | "sample_type": "pretrain", 11 | "num_lines": 0, 12 | "source": "upload" 13 | } 14 | ], 15 | "object": "string" 16 | } 17 | -------------------------------------------------------------------------------- /tests/fixtures/responses/listFineTuningJobs.json: -------------------------------------------------------------------------------- 1 | { 2 | "data": [], 3 | "object": "list", 4 | "total": 0 5 | } 6 | -------------------------------------------------------------------------------- /tests/fixtures/responses/listModels.json: -------------------------------------------------------------------------------- 1 | { 2 | "object": "list", 3 | "data": [ 4 | { 5 | "id": "string", 6 | "object": "model", 7 | "created": 0, 8 | "owned_by": "mistralai", 9 | "name": "string", 10 | "description": "string", 11 | "max_context_length": 32768, 12 | "aliases": [], 13 | "deprecation": "2019-08-24T14:15:22Z", 14 | "capabilities": { 15 | "completion_chat": true, 16 | "completion_fim": false, 17 | "function_calling": true, 18 | "fine_tuning": false, 19 | "vision": false 20 | }, 21 | "type": "base" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /tests/fixtures/responses/retrieveFile.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "497f6eca-6276-4993-bfeb-53cbbbba6f09", 3 | "object": "file", 4 | "bytes": 13000, 5 | "created_at": 1716963433, 6 | "filename": "files_upload.jsonl", 7 | "purpose": "fine-tune", 8 | "sample_type": "pretrain", 9 | "num_lines": 0, 10 | "source": "upload" 11 | } 12 | -------------------------------------------------------------------------------- /tests/fixtures/responses/retrieveFineTuningJob.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", 3 | "auto_start": true, 4 | "hyperparameters": { 5 | "training_steps": 1, 6 | "learning_rate": 0.0001, 7 | "weight_decay": 0.1, 8 | "warmup_fraction": 0.05, 9 | "epochs": 0, 10 | "fim_ratio": 0.9 11 | }, 12 | "model": "open-mistral-7b", 13 | "status": "QUEUED", 14 | "job_type": "string", 15 | "created_at": 0, 16 | "modified_at": 0, 17 | "training_files": [ 18 | "497f6eca-6276-4993-bfeb-53cbbbba6f08" 19 | ], 20 | "validation_files": [], 21 | "object": "job", 22 | "fine_tuned_model": "string", 23 | "suffix": "string", 24 | "integrations": [ 25 | { 26 | "type": "wandb", 27 | "project": "string", 28 | "name": "string", 29 | "run_name": "string" 30 | } 31 | ], 32 | "trained_tokens": 0, 33 | "repositories": [], 34 | "metadata": { 35 | "expected_duration_seconds": 0, 36 | "cost": 0, 37 | "cost_currency": "string", 38 | "train_tokens_per_step": 0, 39 | "train_tokens": 0, 40 | "data_tokens": 0, 41 | "estimated_start_time": 0 42 | }, 43 | "events": [], 44 | "checkpoints": [] 45 | } 46 | -------------------------------------------------------------------------------- /tests/fixtures/responses/retrieveModel.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "string", 3 | "object": "model", 4 | "created": 0, 5 | "owned_by": "mistralai", 6 | "name": "string", 7 | "description": "string", 8 | "max_context_length": 32768, 9 | "aliases": [], 10 | "deprecation": "2019-08-24T14:15:22Z", 11 | "capabilities": { 12 | "completion_chat": true, 13 | "completion_fim": false, 14 | "function_calling": true, 15 | "fine_tuning": false, 16 | "vision": false 17 | }, 18 | "type": "base" 19 | } 20 | -------------------------------------------------------------------------------- /tests/fixtures/responses/startFineTuningJob.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "497f6eca-6276-4993-bfeb-53cbbbba6f08", 3 | "auto_start": true, 4 | "hyperparameters": { 5 | "training_steps": 1, 6 | "learning_rate": 0.0001, 7 | "weight_decay": 0.1, 8 | "warmup_fraction": 0.05, 9 | "epochs": 0, 10 | "fim_ratio": 0.9 11 | }, 12 | "model": "open-mistral-7b", 13 | "status": "QUEUED", 14 | "job_type": "string", 15 | "created_at": 0, 16 | "modified_at": 0, 17 | "training_files": [ 18 | "497f6eca-6276-4993-bfeb-53cbbbba6f08" 19 | ], 20 | "validation_files": [], 21 | "object": "job", 22 | "fine_tuned_model": "string", 23 | "suffix": "string", 24 | "integrations": [ 25 | { 26 | "type": "wandb", 27 | "project": "string", 28 | "name": "string", 29 | "run_name": "string" 30 | } 31 | ], 32 | "trained_tokens": 0, 33 | "repositories": [], 34 | "metadata": { 35 | "expected_duration_seconds": 0, 36 | "cost": 0, 37 | "cost_currency": "string", 38 | "train_tokens_per_step": 0, 39 | "train_tokens": 0, 40 | "data_tokens": 0, 41 | "estimated_start_time": 0 42 | }, 43 | "events": [], 44 | "checkpoints": [] 45 | } 46 | -------------------------------------------------------------------------------- /tests/fixtures/responses/unarchiveModel.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "string", 3 | "object": "model", 4 | "archived": false 5 | } 6 | -------------------------------------------------------------------------------- /tests/fixtures/responses/updateFineTunedModel.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "string", 3 | "object": "model", 4 | "created": 0, 5 | "owned_by": "string", 6 | "root": "string", 7 | "archived": true, 8 | "name": "string", 9 | "description": "string", 10 | "capabilities": { 11 | "completion_chat": true, 12 | "completion_fim": false, 13 | "function_calling": false, 14 | "fine_tuning": false 15 | }, 16 | "max_context_length": 32768, 17 | "aliases": [], 18 | "job": "4bbaedb0-902b-4b27-8218-8f40d3470a54" 19 | } 20 | -------------------------------------------------------------------------------- /tests/fixtures/responses/uploadFile.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "497f6eca-6276-4993-bfeb-53cbbbba6f09", 3 | "object": "file", 4 | "bytes": 13000, 5 | "created_at": 1716963433, 6 | "filename": "files_upload.jsonl", 7 | "purpose": "fine-tune", 8 | "sample_type": "pretrain", 9 | "num_lines": 0, 10 | "source": "upload" 11 | } 12 | --------------------------------------------------------------------------------