├── .github └── workflows │ ├── cs-fixer.yml │ ├── unit-tests.yml │ └── update-changelog.yml ├── .gitignore ├── .php-cs-fixer.dist.php ├── CHANGELOG.md ├── LICENSE ├── README.md ├── composer.json ├── phpunit.xml ├── src ├── V1.php └── V2.php └── tests ├── Pest.php ├── V1Test.php └── V2Test.php /.github/workflows/cs-fixer.yml: -------------------------------------------------------------------------------- 1 | name: CS Fixer 2 | 3 | on: 4 | push: 5 | branches: [ "main", "develop" ] 6 | pull_request: 7 | branches: [ "main", "develop" ] 8 | 9 | permissions: 10 | contents: write 11 | 12 | jobs: 13 | cs-fixer: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - name: Validate composer.json and composer.lock 21 | run: composer validate --strict 22 | 23 | - name: Install dependencies 24 | run: composer install --prefer-dist --no-progress 25 | 26 | - name: Cache Composer packages 27 | id: composer-cache 28 | uses: actions/cache@v3 29 | with: 30 | path: vendor 31 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 32 | restore-keys: | 33 | ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 34 | 35 | - name: Run PHP CS Fixer 36 | uses: docker://oskarstark/php-cs-fixer-ga 37 | with: 38 | args: --config=.php-cs-fixer.dist.php --allow-risky=yes 39 | 40 | - name: Commit changes 41 | uses: stefanzweifel/git-auto-commit-action@v4 42 | with: 43 | commit_message: Fix styling 44 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: 4 | push: 5 | branches: [ "main", "develop" ] 6 | pull_request: 7 | branches: [ "main", "develop" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | unit-tests: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | env: 18 | OPENAI_SECRET_KEY: ${{ secrets.OPENAI_SECRET_KEY }} 19 | OPENAI_ACCESS_TOKEN: ${{ secrets.OPENAI_ACCESS_TOKEN }} 20 | 21 | steps: 22 | - uses: actions/checkout@v3 23 | 24 | - name: Validate composer.json and composer.lock 25 | run: composer validate --strict 26 | 27 | - name: Install dependencies 28 | run: composer install --prefer-dist --no-progress 29 | 30 | - name: Cache Composer packages 31 | id: composer-cache 32 | uses: actions/cache@v3 33 | with: 34 | path: vendor 35 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 36 | restore-keys: | 37 | ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 38 | 39 | - name: Run test suite 40 | run: composer run-script test 41 | -------------------------------------------------------------------------------- /.github/workflows/update-changelog.yml: -------------------------------------------------------------------------------- 1 | name: "Update Changelog" 2 | 3 | on: 4 | release: 5 | types: [released] 6 | 7 | jobs: 8 | update: 9 | runs-on: ubuntu-latest 10 | 11 | permissions: 12 | contents: write 13 | 14 | steps: 15 | - name: Checkout code 16 | uses: actions/checkout@v3 17 | with: 18 | ref: main 19 | 20 | - name: Update Changelog 21 | uses: stefanzweifel/changelog-updater-action@v1 22 | with: 23 | latest-version: ${{ github.event.release.tag_name }} 24 | release-notes: ${{ github.event.release.body }} 25 | 26 | - name: Commit updated CHANGELOG 27 | uses: stefanzweifel/git-auto-commit-action@v4 28 | with: 29 | branch: main 30 | commit_message: Update CHANGELOG 31 | file_pattern: CHANGELOG.md -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | composer.phar 2 | composer.lock 3 | /vendor/ 4 | .idea 5 | 6 | .phpunit.result.cache 7 | .php-cs-fixer.cache 8 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | in([ 5 | __DIR__ . '/src', 6 | __DIR__ . '/tests', 7 | ]) 8 | ->name('*.php') 9 | ->ignoreDotFiles(true) 10 | ->ignoreVCS(true); 11 | 12 | return (new PhpCsFixer\Config()) 13 | ->setRules([ 14 | '@PSR12' => true, 15 | 'array_syntax' => ['syntax' => 'short'], 16 | 'ordered_imports' => ['sort_algorithm' => 'alpha'], 17 | 'no_unused_imports' => true, 18 | 'not_operator_with_successor_space' => true, 19 | 'trailing_comma_in_multiline' => true, 20 | 'ternary_to_null_coalescing' => true, 21 | 'phpdoc_scalar' => true, 22 | 'unary_operator_spaces' => true, 23 | 'binary_operator_spaces' => true, 24 | 'blank_line_before_statement' => [ 25 | 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], 26 | ], 27 | 'phpdoc_single_line_var_spacing' => true, 28 | 'phpdoc_var_without_name' => true, 29 | 'class_attributes_separation' => [ 30 | 'elements' => ['const' => 'one', 'method' => 'one', 'property' => 'one', 'trait_import' => 'none', 'case' => 'none'], 31 | ], 32 | ]) 33 | ->setFinder($finder); 34 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `chatgpt-php` will be documented in this file. 4 | 5 | ## 2.1.2 - 2023-07-17 6 | 7 | 1. fix undefined paid. 8 | 9 | ## 2.1.1 - 2023-07-17 10 | 11 | 1. fix name undefined. 12 | 13 | ## 2.1.0 - 2023-07-06 14 | 15 | 1. **Deprecated: `paid` parameter in `addAccount` method.** 16 | 2. Add arkose_token and fix GPT-4. 17 | 3. `addAccount` method add arkose_token parameter. 18 | 4. `ask` method adds more return values to support continue writing. 19 | 5. Add some new method. 20 | 21 | ## 2.0.4 - 2023-04-25 22 | 23 | 1. Update V1 API 24 | 25 | ## 2.0.3 - 2023-03-31 26 | 27 | 1. Sync V1 V2 return. 28 | 2. Update tests. 29 | 30 | ## 2.0.2 - 2023-03-31 31 | 32 | 1. Bug Fix 33 | 2. Doc update 34 | 35 | ## 2.0.1 - 2023-03-31 36 | 37 | 1. Doc update. 38 | 39 | ## 2.0.0 - 2023-03-31 40 | 41 | 1. Update stream mode return. 42 | 43 | ## 1.8.0 - 2023-03-26 44 | 45 | Update V1 API and change error thrown. 46 | 47 | ## 1.7.2 - 2023-03-17 48 | 49 | Fix V1 PHP7.4 support. 50 | 51 | ## 1.7.1 - 2023-03-15 52 | 53 | V1 support GPT-4 now. 54 | 55 | ## 1.7.0 - 2023-03-10 56 | 57 | Update new V1 api endpoint. 58 | 59 | ## 1.6.2 - 2023-03-09 60 | 61 | Fix errors 62 | 63 | ## 1.6.1 - 2023-03-09 64 | 65 | Add headers for bypass cloudflare 403. 66 | 67 | ## 1.6.0 - 2023-03-09 68 | 69 | Add PHP 7.4 Support. 70 | 71 | ## 1.5.3 - 2023-03-09 72 | 73 | Fix V1 endpoint. (By Pawan's API) 74 | 75 | ## 1.5.2 - 2023-03-06 76 | 77 | Use apps.openai.com as V1 default api endpoint. 78 | 79 | ## 1.5.1 - 2023-03-06 80 | 81 | Remove str_replace. 82 | It may cause return code string error. 83 | 84 | ## 1.5.0 - 2023-03-03 85 | 86 | Fix spelling error. 87 | 88 | ## 1.4.5 - 2023-03-03 89 | 90 | Add checkStreamFields method. 91 | 92 | ## 1.4.4 - 2023-03-03 93 | 94 | Update V2 tests. 95 | 96 | ## 1.4.3 - 2023-03-03 97 | 98 | Update CHANGELOG. 99 | 100 | ## 1.4.2 - 2023-03-03 101 | 102 | Add unit tests and some actions. 103 | Fix some errors. 104 | 105 | ## 1.4.1 - 2023-03-03 106 | 107 | Fix construct error. 108 | 109 | ## 1.4.0 - 2023-03-02 110 | 111 | Update LICENSE. 112 | 113 | ## 1.3.1 - 2023-03-02 114 | 115 | Update README. 116 | 117 | ## 1.3.0 - 2023-03-02 118 | 119 | Rename message return field to answer. 120 | 121 | ## 1.2.0 - 2023-03-02 122 | 123 | Add BaseUrl setting for V2. 124 | 125 | ## 1.1.2 - 2023-03-02 126 | 127 | Update V1 check fields. 128 | 129 | ## 1.1.1 - 2023-03-02 130 | 131 | Add catch for Guzzle. 132 | 133 | ## 1.1.0 - 2023-03-02 134 | 135 | Fix V2 Official API Authorization Error. 136 | 137 | ## 1.0.0 - 2023-03-02 138 | 139 | Ready for production. 140 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 HaoZi-Team 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChatGPT PHP SDK | [Package](https://packagist.org/packages/haozi-team/chatgpt-php) 2 | 3 | [![Total Downloads](https://poser.pugx.org/HaoZi-Team/ChatGPT-PHP/d/total.svg)](https://packagist.org/packages/haozi-team/chatgpt-php) 4 | [![Latest Stable Version](https://poser.pugx.org/HaoZi-Team/ChatGPT-PHP/v/stable.svg)](https://packagist.org/packages/haozi-team/chatgpt-php) 5 | [![License](https://poser.pugx.org/HaoZi-Team/ChatGPT-PHP/license.svg)](https://packagist.org/packages/haozi-team/chatgpt-php) 6 | 7 | Official and Reverse Engineered ChatGPT API for PHP. 8 | 9 | Reconstruct from @acheong08's [ChatGPT](https://github.com/acheong08/ChatGPT) 10 | 11 | # Installation 12 | 13 | `composer require haozi-team/chatgpt-php` 14 | 15 | # V1 Web ChatGPT 16 | 17 | > Uses `chat.openai.com` 18 | > - Free 19 | > - Rate limited 20 | > - Needs Bypassing Cloudflare 21 | 22 | > Default api endpoint is `https://ai.fakeopen.com/api/` by @pengzhile 23 | > 24 | > OpenAI rate limit: 50 requests per hour on free accounts. You can get around it with multi-account cycling 25 | > 26 | > Plus accounts has around 150 requests per hour rate limit 27 | > 28 | > Arkose Token: Recently, OpenAI began to require Arkose Token while bypassing Cloudflare request conversation API, usually the SDK can get it automatically through @pengzhile's API 29 | 30 | ## Configuration 31 | 32 | 1. Create account on [OpenAI's ChatGPT](https://chat.openai.com/) 33 | 2. Save your email and password 34 | 35 | ### Authentication 36 | 37 | #### - Access token 38 | 39 | Login OpenAI account and go to [https://chat.openai.com/api/auth/session](https://chat.openai.com/api/auth/session) 40 | to get your access_token. 41 | 42 | ```json 43 | { 44 | "access_token": "" 45 | } 46 | ``` 47 | 48 | The access_token is valid for 30 days. 49 | 50 | ## Developer API 51 | 52 | ### Basic example 53 | 54 | ```php 55 | addAccount(''); 60 | 61 | $answers = $chatGPT->ask('Hello, how are you?'); 62 | foreach ($answers as $item) { 63 | print_r($item); 64 | } 65 | //Array( 66 | // 'answer' => 'I am fine, thank you.', 67 | // 'conversation_id' => '', 68 | // 'parent_id' => '', 69 | // 'model' => 'text-davinci-002-render-sha', 70 | // 'account' => '0', 71 | //) 72 | ``` 73 | 74 | ### Advanced example 75 | 76 | You can pass "baseUrl" to the first parameter to set a custom API endpoint. 77 | 78 | ```php 79 | Recently released by OpenAI 91 | > - Costs money 92 | 93 | Get API key from https://platform.openai.com/account/api-keys 94 | 95 | ## Developer API 96 | 97 | ### Basic example 98 | 99 | ```php 100 | '); 104 | $chatGPT->addMessage('You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.', 'system'); 105 | 106 | $answers = $chatGPT->ask('Hello, how are you?'); 107 | foreach ($answers as $item) { 108 | print_r($item); 109 | } 110 | ``` 111 | 112 | ### Advanced example 113 | 114 | You can pass "baseUrl" to the second parameter to set a custom API endpoint. 115 | 116 | ```php 117 | ', 'https://api.openai.com/'); 121 | ``` 122 | 123 | You can use `addMessage` to add messages to the conversation. 124 | 125 | ```php 126 | '); 130 | $chatGPT->addMessage('You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.', 'system'); 131 | $chatGPT->addMessage('Hello, how are you?', 'user'); 132 | $chatGPT->addMessage('I am fine, thank you.', 'assistant'); 133 | 134 | $answers = $chatGPT->ask('What did I ask you before?'); 135 | foreach ($answers as $item) { 136 | print_r($item); 137 | } 138 | //Array( 139 | // 'answer' => 'Hello, how are you?', 140 | // 'id' => 'cmpl-xxxxx', 141 | // 'model' => 'gpt-3.5-turbo', 142 | // 'usage' => [ 143 | // "prompt_tokens": 9, 144 | // "completion_tokens": 12, 145 | // "total_tokens": 21, 146 | // ], 147 | //) 148 | ``` 149 | 150 | You can set the `stream` parameter to `true` to get a stream for output answers as they are generated. 151 | 152 | ```php 153 | '); 157 | $chatGPT->addMessage('You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.', 'system'); 158 | 159 | $answers = $chatGPT->ask('Hello, how are you?', null, true);// A Generator 160 | foreach ($answers as $item) { 161 | print_r($item); 162 | } 163 | ``` 164 | 165 | # Disclaimers 166 | 167 | This is not an official OpenAI product. This is a personal project and is not affiliated with OpenAI in any way. Don't 168 | sue me. 169 | 170 | # Credits 171 | 172 | - [acheong08](https://github.com/acheong08) - Python ChatGPT API 173 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "haozi-team/chatgpt-php", 3 | "description": "Real ChatGPT PHP SDK (Not GPT-3)", 4 | "keywords": [ 5 | "chatgpt", 6 | "openai" 7 | ], 8 | "homepage": "https://github.com/HaoZi-Team/ChatGPT-PHP", 9 | "type": "library", 10 | "require": { 11 | "php": ">=7.4", 12 | "guzzlehttp/guzzle": "^7.2", 13 | "ramsey/uuid": "^4.2", 14 | "ext-json": "*", 15 | "ext-curl": "*" 16 | }, 17 | "require-dev": { 18 | "pestphp/pest": "^1.22" 19 | }, 20 | "license": "MIT", 21 | "autoload": { 22 | "psr-4": { 23 | "HaoZiTeam\\ChatGPT\\": "src/" 24 | } 25 | }, 26 | "autoload-dev": { 27 | "psr-4": { 28 | "HaoZiTeam\\ChatGPT\\Tests\\": "tests/" 29 | } 30 | }, 31 | "authors": [ 32 | { 33 | "name": "耗子", 34 | "email": "i@haozi.net" 35 | } 36 | ], 37 | "scripts": { 38 | "test": "vendor/bin/pest", 39 | "test-coverage": "vendor/bin/pest --coverage", 40 | "format": "vendor/bin/php-cs-fixer fix --allow-risky=yes" 41 | }, 42 | "config": { 43 | "allow-plugins": { 44 | "pestphp/pest-plugin": true 45 | }, 46 | "platform": { 47 | "php": "7.4" 48 | } 49 | }, 50 | "prefer-stable": true 51 | } 52 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests 15 | 16 | 17 | 18 | 19 | ./src 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/V1.php: -------------------------------------------------------------------------------- 1 | baseUrl = $baseUrl; 25 | } 26 | 27 | $this->http = new Client([ 28 | 'base_uri' => $this->baseUrl, 29 | 'timeout' => $timeout, 30 | 'stream' => true, 31 | ]); 32 | } 33 | 34 | /** 35 | * 设置账号 36 | * 37 | * @param string $accessToken 38 | * @param mixed $name 39 | * @param string $model 40 | * @param bool $historyAndTrainingDisabled 41 | * @param string|null $arkoseToken 42 | * @return void 43 | */ 44 | public function addAccount(string $accessToken, $name = null, string $model = 'text-davinci-002-render-sha', bool $historyAndTrainingDisabled = false, string $arkoseToken = null): void 45 | { 46 | if ($name === null) { 47 | $this->accounts[] = [ 48 | 'access_token' => $accessToken, 49 | 'model' => $model, 50 | 'history_and_training_disabled' => $historyAndTrainingDisabled, 51 | 'arkose_token' => $arkoseToken, 52 | ]; 53 | if ($arkoseToken === null) { 54 | try { 55 | $this->accounts[count($this->accounts) - 1]['arkose_token'] = $this->getArkoseToken(); 56 | } catch (Exception $e) { 57 | $this->accounts[count($this->accounts) - 1]['arkose_token'] = ''; 58 | } 59 | } 60 | } else { 61 | $this->accounts[$name] = [ 62 | 'access_token' => $accessToken, 63 | 'model' => $model, 64 | 'history_and_training_disabled' => $historyAndTrainingDisabled, 65 | 'arkose_token' => $arkoseToken, 66 | ]; 67 | if ($arkoseToken === null) { 68 | try { 69 | $this->accounts[$name]['arkose_token'] = $this->getArkoseToken(); 70 | } catch (Exception $e) { 71 | $this->accounts[$name]['arkose_token'] = ''; 72 | } 73 | } 74 | } 75 | } 76 | 77 | /** 78 | * 获取账号 79 | * 80 | * @param string $name 81 | * 82 | * @return array 83 | */ 84 | public function getAccount(string $name): array 85 | { 86 | return $this->accounts[$name]; 87 | } 88 | 89 | /** 90 | * 获取所有账号 91 | * @return array 92 | */ 93 | public function getAccounts(): array 94 | { 95 | return $this->accounts; 96 | } 97 | 98 | /** 99 | * 发送消息 100 | * 101 | * @param string $prompt 102 | * @param string|null $conversationId 103 | * @param string|null $parentId 104 | * @param mixed $account 105 | * @param bool $stream 106 | * 107 | * @return Generator 108 | * @throws Exception|GuzzleException 109 | */ 110 | public function ask( 111 | string $prompt, 112 | string $conversationId = null, 113 | string $parentId = null, 114 | $account = null, 115 | bool $stream = false 116 | ): Generator { 117 | // 如果账号为空,则随机选择一个账号 118 | if ($account === null) { 119 | $account = array_rand($this->accounts); 120 | 121 | try { 122 | $token = $this->accessTokenToJWT($this->accounts[$account]['access_token']); 123 | } catch (Exception $e) { 124 | throw new Exception("Account " . $account . " is invalid"); 125 | } 126 | } else { 127 | $token = isset($this->accounts[$account]['access_token']) ? $this->accessTokenToJWT($this->accounts[$account]['access_token']) : null; 128 | } 129 | 130 | // 如果账号为空,则抛出异常 131 | if ($token === null) { 132 | throw new Exception("No account available"); 133 | } 134 | 135 | // 设置了父消息ID,必须设置会话ID 136 | if ($parentId !== null && $conversationId === null) { 137 | throw new Exception("conversation_id must be set once parent_id is set"); 138 | } 139 | 140 | // 如果会话ID与父消息ID都为空,则开启新的会话 141 | if ($conversationId === null && $parentId === null) { 142 | $parentId = (string)Uuid::uuid4(); 143 | } 144 | 145 | // 如果会话ID不为空,但是父消息ID为空,则尝试从ChatGPT获取历史记录 146 | if ($conversationId !== null && $parentId === null) { 147 | try { 148 | $response = $this->http->get('conversation/' . $conversationId, [ 149 | 'headers' => [ 150 | 'Authorization' => $token, 151 | 'Content-Type' => 'application/json', 152 | 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63', 153 | 'Referer' => 'https://chat.openai.com/chat', 154 | ], 155 | ]); 156 | } catch (GuzzleException $e) { 157 | throw new Exception("Request failed: " . $e->getMessage()); 158 | } 159 | 160 | $response = json_decode($response->getBody()->getContents(), true); 161 | if (isset($response['current_node'])) { 162 | // 如果获取到了父消息ID,则使用该父消息ID 163 | $conversationId = $response['current_node']; 164 | } else { 165 | // 如果没有获取到父消息ID,则开启新的会话 166 | $conversationId = null; 167 | $parentId = (string)Uuid::uuid4(); 168 | } 169 | } 170 | 171 | $data = [ 172 | 'action' => 'next', 173 | 'messages' => [ 174 | [ 175 | 'id' => (string)Uuid::uuid4(), 176 | 'role' => 'user', 177 | 'author' => ['role' => 'user'], 178 | 'content' => ['content_type' => 'text', 'parts' => [$prompt]], 179 | ], 180 | ], 181 | 'suggestions' => [], 182 | 'conversation_id' => $conversationId, 183 | 'parent_message_id' => $parentId, 184 | 'model' => $this->accounts[$account]['model'], 185 | 'arkose_token' => $this->accounts[$account]['arkose_token'], 186 | 'history_and_training_disabled' => $this->accounts[$account]['history_and_training_disabled'], 187 | 'timezone_offset_min' => -480, 188 | ]; 189 | 190 | try { 191 | $response = $this->http->post( 192 | 'conversation', 193 | [ 194 | 'json' => $data, 195 | 'headers' => [ 196 | 'Authorization' => $token, 197 | 'Accept' => 'text/event-stream', 198 | 'Content-Type' => 'application/json', 199 | 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63', 200 | 'X-Openai-Assistant-App-Id' => '', 201 | 'Connection' => 'close', 202 | 'Accept-Language' => 'en-US,en;q=0.9', 203 | 'Referer' => 'https://chat.openai.com/chat', 204 | ], 205 | 'stream' => true, 206 | ] 207 | ); 208 | } catch (RequestException $e) { 209 | if ($e->hasResponse()) { 210 | throw new Exception(Psr7\Message::toString($e->getResponse())); 211 | } else { 212 | throw new Exception($e->getMessage()); 213 | } 214 | } 215 | 216 | $answer = ''; 217 | $conversationId = ''; 218 | $messageId = ''; 219 | $model = ''; 220 | 221 | // 流模式下,返回一个生成器 222 | if ($stream) { 223 | $data = $response->getBody(); 224 | while (! $data->eof()) { 225 | $raw = Psr7\Utils::readLine($data); 226 | $line = self::formatStreamMessage($raw); 227 | if (self::checkFields($line)) { 228 | $answer = $line['message']['content']['parts'][0]; 229 | $conversationId = $line['conversation_id'] ?? null; 230 | $messageId = $line['message']['id'] ?? null; 231 | $model = $line["message"]["metadata"]["model_slug"] ?? null; 232 | $finish_details = $line["message"]["metadata"]["finish_details"]['type'] ?? null; 233 | $end_turn = $line["message"]["end_turn"] ?? true; 234 | $recipient = $line["message"]["recipient"] ?? "all"; 235 | $citations = $line["message"]["metadata"]["citations"] ?? []; 236 | 237 | yield [ 238 | "answer" => $answer, 239 | "id" => $messageId, 240 | 'conversation_id' => $conversationId, 241 | "model" => $model, 242 | "account" => $account, 243 | 'finish_details' => $finish_details, 244 | 'end_turn' => $end_turn, 245 | 'recipient' => $recipient, 246 | 'citations' => $citations, 247 | ]; 248 | } 249 | unset($raw, $line); 250 | } 251 | } else { 252 | foreach (explode("\n", $response->getBody()) as $line) { 253 | $line = trim($line); 254 | if ($line === 'Internal Server Error') { 255 | throw new Exception($line); 256 | } 257 | if ($line === '') { 258 | continue; 259 | } 260 | 261 | $line = $this->formatStreamMessage($line); 262 | 263 | if (! $this->checkFields($line)) { 264 | if (isset($line["detail"]) && $line["detail"] === "Too many requests in 1 hour. Try again later.") { 265 | throw new Exception("Rate limit exceeded"); 266 | } 267 | if (isset($line["detail"]) && $line["detail"] === "Conversation not found") { 268 | throw new Exception("Conversation not found"); 269 | } 270 | if (isset($line["detail"]) && $line["detail"] === "Something went wrong, please try reloading the conversation.") { 271 | throw new Exception("Something went wrong, please try reloading the conversation."); 272 | } 273 | if (isset($line["detail"]) && $line["detail"] === "invalid_api_key") { 274 | throw new Exception("Invalid access token"); 275 | } 276 | if (isset($line["detail"]) && $line["detail"] === "invalid_token") { 277 | throw new Exception("Invalid access token"); 278 | } 279 | 280 | continue; 281 | } 282 | 283 | if ($line['message']['content']['parts'][0] === $prompt) { 284 | continue; 285 | } 286 | 287 | $answer = $line['message']['content']['parts'][0]; 288 | $conversationId = $line['conversation_id'] ?? null; 289 | $messageId = $line['message']['id'] ?? null; 290 | $model = $line["message"]["metadata"]["model_slug"] ?? null; 291 | $finish_details = $line["message"]["metadata"]["finish_details"]['type'] ?? null; 292 | $end_turn = $line["message"]["end_turn"] ?? true; 293 | $recipient = $line["message"]["recipient"] ?? "all"; 294 | $citations = $line["message"]["metadata"]["citations"] ?? []; 295 | } 296 | 297 | yield [ 298 | 'answer' => $answer, 299 | 'id' => $messageId, 300 | 'conversation_id' => $conversationId, 301 | 'model' => $model, 302 | 'account' => $account, 303 | 'finish_details' => $finish_details, 304 | 'end_turn' => $end_turn, 305 | 'recipient' => $recipient, 306 | 'citations' => $citations, 307 | ]; 308 | } 309 | } 310 | 311 | /** 312 | * 续写 313 | * 314 | * @param string $prompt 315 | * @param string|null $conversationId 316 | * @param string|null $parentId 317 | * @param mixed $account 318 | * @param bool $stream 319 | * 320 | * @return Generator 321 | * @throws Exception 322 | */ 323 | public function continueWrite( 324 | string $prompt, 325 | string $conversationId = null, 326 | string $parentId = null, 327 | $account = null, 328 | bool $stream = false 329 | ): Generator { 330 | if ($account === null) { 331 | throw new Exception("Continue write must set account"); 332 | } else { 333 | $token = isset($this->accounts[$account]['access_token']) ? $this->accessTokenToJWT($this->accounts[$account]['access_token']) : null; 334 | } 335 | 336 | if ($token === null) { 337 | throw new Exception("No account available"); 338 | } 339 | 340 | if ($parentId == null || $conversationId === null) { 341 | throw new Exception("Continue write must set conversationId and parentId"); 342 | } 343 | 344 | $data = [ 345 | 'action' => 'continue', 346 | 'conversation_id' => $conversationId, 347 | 'parent_message_id' => $parentId, 348 | 'model' => $this->accounts[$account]['model'], 349 | 'arkose_token' => $this->accounts[$account]['arkose_token'], 350 | 'history_and_training_disabled' => $this->accounts[$account]['history_and_training_disabled'], 351 | ]; 352 | 353 | try { 354 | $response = $this->http->post( 355 | 'conversation', 356 | [ 357 | 'json' => $data, 358 | 'headers' => [ 359 | 'Authorization' => $token, 360 | 'Accept' => 'text/event-stream', 361 | 'Content-Type' => 'application/json', 362 | 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63', 363 | 'X-Openai-Assistant-App-Id' => '', 364 | 'Connection' => 'close', 365 | 'Accept-Language' => 'en-US,en;q=0.9', 366 | 'Referer' => 'https://chat.openai.com/chat', 367 | ], 368 | 'stream' => true, 369 | ] 370 | ); 371 | } catch (RequestException $e) { 372 | if ($e->hasResponse()) { 373 | throw new Exception(Psr7\Message::toString($e->getResponse())); 374 | } else { 375 | throw new Exception($e->getMessage()); 376 | } 377 | } 378 | 379 | $answer = ''; 380 | $conversationId = ''; 381 | $messageId = ''; 382 | $model = ''; 383 | 384 | // 流模式下,返回一个生成器 385 | if ($stream) { 386 | $data = $response->getBody(); 387 | while (! $data->eof()) { 388 | $raw = Psr7\Utils::readLine($data); 389 | $line = self::formatStreamMessage($raw); 390 | if (self::checkFields($line)) { 391 | $answer = $line['message']['content']['parts'][0]; 392 | $conversationId = $line['conversation_id'] ?? null; 393 | $messageId = $line['message']['id'] ?? null; 394 | $model = $line["message"]["metadata"]["model_slug"] ?? null; 395 | $finish_details = $line["message"]["metadata"]["finish_details"]['type'] ?? null; 396 | $end_turn = $line["message"]["end_turn"] ?? true; 397 | $recipient = $line["message"]["recipient"] ?? "all"; 398 | $citations = $line["message"]["metadata"]["citations"] ?? []; 399 | 400 | yield [ 401 | "answer" => $answer, 402 | "id" => $messageId, 403 | 'conversation_id' => $conversationId, 404 | "model" => $model, 405 | "account" => $account, 406 | 'finish_details' => $finish_details, 407 | 'end_turn' => $end_turn, 408 | 'recipient' => $recipient, 409 | 'citations' => $citations, 410 | ]; 411 | } 412 | unset($raw, $line); 413 | } 414 | } else { 415 | foreach (explode("\n", $response->getBody()) as $line) { 416 | $line = trim($line); 417 | if ($line === 'Internal Server Error') { 418 | throw new Exception($line); 419 | } 420 | if ($line === '') { 421 | continue; 422 | } 423 | 424 | $line = $this->formatStreamMessage($line); 425 | 426 | if (! $this->checkFields($line)) { 427 | if (isset($line["detail"]) && $line["detail"] === "Too many requests in 1 hour. Try again later.") { 428 | throw new Exception("Rate limit exceeded"); 429 | } 430 | if (isset($line["detail"]) && $line["detail"] === "Conversation not found") { 431 | throw new Exception("Conversation not found"); 432 | } 433 | if (isset($line["detail"]) && $line["detail"] === "Something went wrong, please try reloading the conversation.") { 434 | throw new Exception("Something went wrong, please try reloading the conversation."); 435 | } 436 | if (isset($line["detail"]) && $line["detail"] === "invalid_api_key") { 437 | throw new Exception("Invalid access token"); 438 | } 439 | if (isset($line["detail"]) && $line["detail"] === "invalid_token") { 440 | throw new Exception("Invalid access token"); 441 | } 442 | 443 | continue; 444 | } 445 | 446 | if ($line['message']['content']['parts'][0] === $prompt) { 447 | continue; 448 | } 449 | 450 | $answer = $line['message']['content']['parts'][0]; 451 | $conversationId = $line['conversation_id'] ?? null; 452 | $messageId = $line['message']['id'] ?? null; 453 | $model = $line["message"]["metadata"]["model_slug"] ?? null; 454 | $finish_details = $line["message"]["metadata"]["finish_details"]['type'] ?? null; 455 | $end_turn = $line["message"]["end_turn"] ?? true; 456 | $recipient = $line["message"]["recipient"] ?? "all"; 457 | $citations = $line["message"]["metadata"]["citations"] ?? []; 458 | } 459 | 460 | yield [ 461 | 'answer' => $answer, 462 | 'id' => $messageId, 463 | 'conversation_id' => $conversationId, 464 | 'model' => $model, 465 | 'account' => $account, 466 | 'finish_details' => $finish_details, 467 | 'end_turn' => $end_turn, 468 | 'recipient' => $recipient, 469 | 'citations' => $citations, 470 | ]; 471 | } 472 | } 473 | 474 | /** 475 | * 获取会话列表 476 | * 477 | * @param int $offset 478 | * @param int $limit 479 | * @param mixed $account 480 | * 481 | * @return array 482 | * @throws Exception 483 | */ 484 | public function getConversations(int $offset = 0, int $limit = 20, $account = 0): array 485 | { 486 | try { 487 | $token = $this->accessTokenToJWT($this->accounts[$account]['access_token']); 488 | } catch (Exception $e) { 489 | throw new Exception("Invalid account"); 490 | } 491 | 492 | try { 493 | $response = $this->http->get('conversations', [ 494 | 'headers' => [ 495 | 'Authorization' => $token, 496 | 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63', 497 | 'Referer' => 'https://chat.openai.com/chat', 498 | ], 499 | 'query' => [ 500 | 'offset' => $offset, 501 | 'limit' => $limit, 502 | ], 503 | ])->getBody()->getContents(); 504 | } catch (GuzzleException $e) { 505 | throw new Exception($e->getMessage()); 506 | } 507 | 508 | $data = json_decode($response, true); 509 | if (json_last_error() !== JSON_ERROR_NONE) { 510 | throw new Exception('Response is not json'); 511 | } 512 | 513 | if (! isset($data['items'])) { 514 | throw new Exception('Field missing'); 515 | } 516 | 517 | return $data['items']; 518 | } 519 | 520 | /** 521 | * 获取会话消息列表 522 | * 523 | * @param string $conversationId 524 | * @param mixed $account 525 | * 526 | * @return array 527 | * @throws Exception 528 | */ 529 | public function getConversationMessages(string $conversationId, $account = 0): array 530 | { 531 | try { 532 | $token = $this->accessTokenToJWT($this->accounts[$account]['access_token']); 533 | } catch (Exception $e) { 534 | throw new Exception("Invalid account"); 535 | } 536 | 537 | try { 538 | $response = $this->http->get('conversation/' . $conversationId, [ 539 | 'headers' => [ 540 | 'Authorization' => $token, 541 | 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63', 542 | 'Referer' => 'https://chat.openai.com/chat', 543 | ], 544 | ])->getBody()->getContents(); 545 | } catch (GuzzleException $e) { 546 | throw new Exception($e->getMessage()); 547 | } 548 | 549 | $data = json_decode($response, true); 550 | if (json_last_error() !== JSON_ERROR_NONE) { 551 | throw new Exception('Response is not json'); 552 | } 553 | 554 | return $data; 555 | } 556 | 557 | /** 558 | * 生成会话标题 559 | * 560 | * @param string $conversationId 561 | * @param string $messageId 562 | * @param mixed $account 563 | * 564 | * @return bool 565 | * @throws Exception 566 | */ 567 | public function generateConversationTitle(string $conversationId, string $messageId, $account = 0): bool 568 | { 569 | try { 570 | $token = $this->accessTokenToJWT($this->accounts[$account]['access_token']); 571 | } catch (Exception $e) { 572 | throw new Exception("Invalid account"); 573 | } 574 | 575 | try { 576 | $response = $this->http->post('conversation/gen_title/' . $conversationId, [ 577 | 'headers' => [ 578 | 'Authorization' => $token, 579 | 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63', 580 | 'Referer' => 'https://chat.openai.com/chat', 581 | ], 582 | 'json' => [ 583 | 'message_id' => $messageId, 584 | 'model' => 'text-davinci-002-render', 585 | ], 586 | ])->getBody()->getContents(); 587 | } catch (GuzzleException $e) { 588 | throw new Exception($e->getMessage()); 589 | } 590 | 591 | $data = json_decode($response, true); 592 | if (json_last_error() !== JSON_ERROR_NONE) { 593 | throw new Exception('Response is not json'); 594 | } 595 | 596 | if (isset($data['title'])) { 597 | return true; 598 | } 599 | 600 | return false; 601 | } 602 | 603 | /** 604 | * 修改会话标题 605 | * 606 | * @param string $conversationId 607 | * @param string $title 608 | * @param mixed $account 609 | * 610 | * @return bool 611 | * @throws Exception 612 | */ 613 | public function updateConversationTitle(string $conversationId, string $title, $account = 0): bool 614 | { 615 | try { 616 | $token = $this->accessTokenToJWT($this->accounts[$account]['access_token']); 617 | } catch (Exception $e) { 618 | throw new Exception("Invalid account"); 619 | } 620 | 621 | try { 622 | $response = $this->http->patch('conversation/' . $conversationId, [ 623 | 'headers' => [ 624 | 'Authorization' => $token, 625 | 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63', 626 | 'Referer' => 'https://chat.openai.com/chat', 627 | ], 628 | 'json' => [ 629 | 'title' => $title, 630 | ], 631 | ])->getBody()->getContents(); 632 | } catch (GuzzleException $e) { 633 | throw new Exception($e->getMessage()); 634 | } 635 | 636 | $data = json_decode($response, true); 637 | if (json_last_error() !== JSON_ERROR_NONE) { 638 | throw new Exception('Response is not json'); 639 | } 640 | 641 | if (isset($data['success']) && $data['success'] === true) { 642 | return true; 643 | } 644 | 645 | return false; 646 | } 647 | 648 | /** 649 | * 删除会话 650 | * 651 | * @param string $conversationId 652 | * @param mixed $account 653 | * 654 | * @return bool 655 | * @throws Exception 656 | */ 657 | public function deleteConversation(string $conversationId, $account = 0): bool 658 | { 659 | try { 660 | $token = $this->accessTokenToJWT($this->accounts[$account]['access_token']); 661 | } catch (Exception $e) { 662 | throw new Exception("Invalid account"); 663 | } 664 | 665 | try { 666 | $response = $this->http->patch('conversation/' . $conversationId, [ 667 | 'headers' => [ 668 | 'Authorization' => $token, 669 | 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63', 670 | 'Referer' => 'https://chat.openai.com/chat', 671 | ], 672 | 'json' => [ 673 | 'is_visible' => false, 674 | ], 675 | ])->getBody()->getContents(); 676 | } catch (GuzzleException $e) { 677 | throw new Exception($e->getMessage()); 678 | } 679 | 680 | $data = json_decode($response, true); 681 | if (json_last_error() !== JSON_ERROR_NONE) { 682 | throw new Exception('Response is not json'); 683 | } 684 | 685 | if (isset($data['success']) && $data['success'] === true) { 686 | return true; 687 | } 688 | 689 | return false; 690 | } 691 | 692 | /** 693 | * 清空会话 694 | * 695 | * @param mixed $account 696 | * 697 | * @return bool 698 | * @throws Exception 699 | */ 700 | public function clearConversations($account = 0): bool 701 | { 702 | try { 703 | $token = $this->accessTokenToJWT($this->accounts[$account]['access_token']); 704 | } catch (Exception $e) { 705 | throw new Exception("Invalid account"); 706 | } 707 | 708 | try { 709 | $response = $this->http->patch('conversations', [ 710 | 'headers' => [ 711 | 'Authorization' => $token, 712 | 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63', 713 | 'Referer' => 'https://chat.openai.com/chat', 714 | ], 715 | 'json' => [ 716 | 'is_visible' => false, 717 | ], 718 | ])->getBody()->getContents(); 719 | } catch (GuzzleException $e) { 720 | throw new Exception($e->getMessage()); 721 | } 722 | 723 | $data = json_decode($response, true); 724 | if (json_last_error() !== JSON_ERROR_NONE) { 725 | throw new Exception('Response is not json'); 726 | } 727 | 728 | if (isset($data['success']) && $data['success'] === true) { 729 | return true; 730 | } 731 | 732 | return false; 733 | } 734 | 735 | /** 736 | * 获取插件列表 737 | * 738 | * @param int $offset 739 | * @param int $limit 740 | * @param string $status 741 | * @param mixed $account 742 | * 743 | * @return array 744 | */ 745 | public function getPlugins(int $offset = 0, int $limit = 250, string $status = 'approved', $account = 0): array 746 | { 747 | try { 748 | $token = $this->accessTokenToJWT($this->accounts[$account]['access_token']); 749 | } catch (Exception $e) { 750 | throw new Exception("Invalid account"); 751 | } 752 | 753 | try { 754 | $response = $this->http->get('aip/p', [ 755 | 'headers' => [ 756 | 'Authorization' => $token, 757 | 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63', 758 | 'Referer' => 'https://chat.openai.com/chat', 759 | ], 760 | 'query' => [ 761 | 'offset' => $offset, 762 | 'limit' => $limit, 763 | 'status' => $status, 764 | ], 765 | ])->getBody()->getContents(); 766 | } catch (GuzzleException $e) { 767 | return []; 768 | } 769 | 770 | $data = json_decode($response, true); 771 | if (json_last_error() !== JSON_ERROR_NONE) { 772 | return []; 773 | } 774 | 775 | return $data; 776 | } 777 | 778 | /** 779 | * 安装插件 780 | * 781 | * @param string $pluginId 782 | * @param mixed $account 783 | * 784 | * @return bool 785 | */ 786 | public function installPlugin(string $pluginId, $account = 0): bool 787 | { 788 | try { 789 | $token = $this->accessTokenToJWT($this->accounts[$account]['access_token']); 790 | } catch (Exception $e) { 791 | throw new Exception("Invalid account"); 792 | } 793 | 794 | try { 795 | $response = $this->http->patch('aip/p/' . $pluginId . '/user-settings', [ 796 | 'headers' => [ 797 | 'Authorization' => $token, 798 | 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63', 799 | 'Referer' => 'https://chat.openai.com/chat', 800 | ], 801 | ])->getBody()->getContents(); 802 | } catch (GuzzleException $e) { 803 | return false; 804 | } 805 | 806 | $data = json_decode($response, true); 807 | if (json_last_error() !== JSON_ERROR_NONE) { 808 | return false; 809 | } 810 | 811 | return true; 812 | } 813 | 814 | /** 815 | * 获取未验证插件 816 | * 817 | * @param string $domain 818 | * @param mixed $account 819 | * 820 | * @return array 821 | */ 822 | public function getUnverifiedPlugins(string $domain = '', $account = 0): array 823 | { 824 | try { 825 | $token = $this->accessTokenToJWT($this->accounts[$account]['access_token']); 826 | } catch (Exception $e) { 827 | throw new Exception("Invalid account"); 828 | } 829 | 830 | try { 831 | $response = $this->http->get('aip/p/domain', [ 832 | 'headers' => [ 833 | 'Authorization' => $token, 834 | 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63', 835 | 'Referer' => 'https://chat.openai.com/chat', 836 | ], 837 | 'query' => [ 838 | 'domain' => $domain, 839 | ], 840 | ])->getBody()->getContents(); 841 | } catch (GuzzleException $e) { 842 | return []; 843 | } 844 | 845 | $data = json_decode($response, true); 846 | if (json_last_error() !== JSON_ERROR_NONE) { 847 | return []; 848 | } 849 | 850 | return $data; 851 | } 852 | 853 | /** 854 | * 设置保存聊天记录与训练 855 | * 856 | * @param bool $save 857 | * @param mixed $account 858 | * 859 | * @return bool 860 | */ 861 | public function setChatHistoryAndTraining(bool $save, $account = 0): bool 862 | { 863 | try { 864 | $token = $this->accessTokenToJWT($this->accounts[$account]['access_token']); 865 | } catch (Exception $e) { 866 | throw new Exception("Invalid account"); 867 | } 868 | 869 | try { 870 | $response = $this->http->get('models', [ 871 | 'headers' => [ 872 | 'Authorization' => $token, 873 | 'User-Agent' => 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/110.0.0.0 Safari/537.36 Edg/110.0.1587.63', 874 | 'Referer' => 'https://chat.openai.com/chat', 875 | ], 876 | 'query' => [ 877 | 'history_and_training_disabled' => ! $save, 878 | ], 879 | ])->getBody()->getContents(); 880 | } catch (GuzzleException $e) { 881 | return false; 882 | } 883 | 884 | $data = json_decode($response, true); 885 | if (json_last_error() !== JSON_ERROR_NONE) { 886 | return false; 887 | } 888 | 889 | return true; 890 | } 891 | 892 | /** 893 | * 检查响应行是否包含必要的字段 894 | * 895 | * @param mixed $line 896 | * 897 | * @return bool 898 | */ 899 | public function checkFields($line): bool 900 | { 901 | return isset($line['message']['content']['parts'][0]) 902 | && isset($line['conversation_id']) 903 | && isset($line['message']['id']); 904 | } 905 | 906 | /** 907 | * 格式化流消息为数组 908 | * 909 | * @param string $line 910 | * 911 | * @return array|false 912 | */ 913 | public function formatStreamMessage(string $line) 914 | { 915 | preg_match('/data: (.*)/', $line, $matches); 916 | if (empty($matches[1])) { 917 | return false; 918 | } 919 | 920 | $line = $matches[1]; 921 | $data = json_decode($line, true); 922 | 923 | if (json_last_error() !== JSON_ERROR_NONE) { 924 | return false; 925 | } 926 | 927 | return $data; 928 | } 929 | 930 | /** 931 | * access_token 转换为 JWT 932 | * 933 | * @param string $accessToken 934 | * 935 | * @return string 936 | * @throws Exception 937 | */ 938 | private function accessTokenToJWT(string $accessToken): string 939 | { 940 | return 'Bearer ' . $accessToken; 941 | } 942 | 943 | /** 944 | * 获取arkose_token 945 | * 946 | * @return string 947 | * @throws Exception 948 | */ 949 | public function getArkoseToken(): string 950 | { 951 | $ch = curl_init(); 952 | curl_setopt($ch, CURLOPT_URL, 'https://ai.fakeopen.com/api/arkose/token'); 953 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); 954 | $response = curl_exec($ch); 955 | curl_close($ch); 956 | 957 | if ($response === false) { 958 | throw new Exception('Request arkose token failed'); 959 | } 960 | 961 | $data = json_decode($response, true); 962 | if (json_last_error() !== JSON_ERROR_NONE) { 963 | throw new Exception('Request arkose response is not json'); 964 | } 965 | 966 | if (! isset($data['token'])) { 967 | throw new Exception('Request arkose token failed'); 968 | } 969 | 970 | return $data['token']; 971 | } 972 | } 973 | -------------------------------------------------------------------------------- /src/V2.php: -------------------------------------------------------------------------------- 1 | key = 'Bearer '.$key; 37 | if ($baseUrl) { 38 | $this->baseUrl = $baseUrl; 39 | } 40 | if ($model) { 41 | $this->model = $model; 42 | } 43 | if ($temperature) { 44 | $this->temperature = $temperature; 45 | } 46 | if ($topP) { 47 | $this->topP = $topP; 48 | } 49 | 50 | $this->http = new Client([ 51 | 'base_uri' => $this->baseUrl, 52 | 'timeout' => $timeout, 53 | 'stream' => true, 54 | ]); 55 | } 56 | 57 | /** 58 | * 添加消息 59 | * 60 | * @param string $message 61 | * @param string $role 62 | * 63 | * @return void 64 | */ 65 | public function addMessage(string $message, string $role = 'user'): void 66 | { 67 | $this->messages[] = [ 68 | 'role' => $role, 69 | 'content' => $message, 70 | ]; 71 | } 72 | 73 | /** 74 | * 发送消息 75 | * 76 | * @param string $prompt 77 | * @param string|null $user 78 | * @param bool $stream 79 | * 80 | * @return Generator 81 | * @throws Exception|GuzzleException 82 | */ 83 | public function ask(string $prompt, string $user = null, bool $stream = false): Generator 84 | { 85 | // 将消息添加到消息列表中 86 | $this->addMessage($prompt); 87 | 88 | $data = [ 89 | 'model' => $this->model, 90 | 'messages' => $this->messages, 91 | 'stream' => $stream, 92 | 'temperature' => $this->temperature, 93 | 'top_p' => $this->topP, 94 | 'n' => 1, 95 | 'user' => $user ?? 'chatgpt-php', 96 | ]; 97 | 98 | try { 99 | $response = $this->http->post( 100 | 'v1/chat/completions', 101 | [ 102 | 'json' => $data, 103 | 'headers' => [ 104 | 'Authorization' => $this->key, 105 | ], 106 | 'stream' => $stream, 107 | ] 108 | ); 109 | } catch (RequestException $e) { 110 | if ($e->hasResponse()) { 111 | throw new Exception(Psr7\Message::toString($e->getResponse())); 112 | } else { 113 | throw new Exception($e->getMessage()); 114 | } 115 | } 116 | 117 | $answer = ''; 118 | 119 | // 流模式下,返回一个生成器 120 | if ($stream) { 121 | $data = $response->getBody(); 122 | while (! $data->eof()) { 123 | $raw = Psr7\Utils::readLine($data); 124 | $line = self::formatStreamMessage($raw); 125 | if (self::checkStreamFields($line)) { 126 | $answer = $line['choices'][0]['delta']['content']; 127 | $messageId = $line['id']; 128 | 129 | yield [ 130 | "answer" => $answer, 131 | "id" => $messageId, 132 | "model" => $this->model, 133 | ]; 134 | } 135 | unset($raw, $line); 136 | } 137 | $this->addMessage($answer, 'assistant'); 138 | } else { 139 | $data = json_decode($response->getBody()->getContents(), true); 140 | if (json_last_error() !== JSON_ERROR_NONE) { 141 | throw new Exception('Response is not json'); 142 | } 143 | 144 | if (! $this->checkFields($data)) { 145 | throw new Exception('Field missing'); 146 | } 147 | 148 | $answer = $data['choices'][0]['message']['content']; 149 | $this->addMessage($answer, 'assistant'); 150 | 151 | yield [ 152 | 'answer' => $answer, 153 | 'id' => $data['id'], 154 | 'model' => $this->model, 155 | 'usage' => $data['usage'], 156 | ]; 157 | } 158 | } 159 | 160 | /** 161 | * 检查响应行是否包含必要的字段 162 | * 163 | * @param mixed $line 164 | * 165 | * @return bool 166 | */ 167 | public function checkFields($line): bool 168 | { 169 | return isset($line['choices'][0]['message']['content']) && isset($line['id']) && isset($line['usage']); 170 | } 171 | 172 | /** 173 | * 检查流响应行是否包含必要的字段 174 | * 175 | * @param mixed $line 176 | * 177 | * @return bool 178 | */ 179 | public function checkStreamFields($line): bool 180 | { 181 | return isset($line['choices'][0]['delta']['content']) && isset($line['id']); 182 | } 183 | 184 | /** 185 | * 格式化流消息为数组 186 | * 187 | * @param string $line 188 | * 189 | * @return mixed 190 | */ 191 | public function formatStreamMessage(string $line) 192 | { 193 | preg_match('/data: (.*)/', $line, $matches); 194 | if (empty($matches[1])) { 195 | return false; 196 | } 197 | 198 | $line = $matches[1]; 199 | $data = json_decode($line, true); 200 | 201 | if (json_last_error() !== JSON_ERROR_NONE) { 202 | return false; 203 | } 204 | 205 | return $data; 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /tests/Pest.php: -------------------------------------------------------------------------------- 1 | in('Feature'); 15 | 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | Expectations 19 | |-------------------------------------------------------------------------- 20 | | 21 | | When you're writing tests, you often need to check that values meet certain conditions. The 22 | | "expect()" function gives you access to a set of "expectations" methods that you can use 23 | | to assert different things. Of course, you may extend the Expectation API at any time. 24 | | 25 | */ 26 | 27 | expect()->extend('toBeOne', function () { 28 | return $this->toBe(1); 29 | }); 30 | 31 | /* 32 | |-------------------------------------------------------------------------- 33 | | Functions 34 | |-------------------------------------------------------------------------- 35 | | 36 | | While Pest is very powerful out-of-the-box, you may have some testing code specific to your 37 | | project that you don't want to repeat in every file. Here you can also expose helpers as 38 | | global functions to help you to reduce the number of lines of code in your test files. 39 | | 40 | */ 41 | 42 | function something() 43 | { 44 | // .. 45 | } 46 | -------------------------------------------------------------------------------- /tests/V1Test.php: -------------------------------------------------------------------------------- 1 | addAccount($accessToken, 0, 'gpt-4'); 8 | $test = $chatGPT->ask('Hello'); 9 | foreach ($test as $answer) { 10 | $conversationId = $answer['conversation_id']; 11 | $parentId = $answer['id']; 12 | } 13 | 14 | it('should get a new conversation', function () use ($chatGPT) { 15 | $return = $chatGPT->ask('Hello'); 16 | foreach ($return as $answer) { 17 | $this->assertArrayHasKey('answer', $answer); 18 | } 19 | })->group('working'); 20 | 21 | it('should get a conversations array', function () use ($chatGPT) { 22 | $return = $chatGPT->getConversations(); 23 | $this->assertIsArray($return); 24 | })->group('working'); 25 | 26 | it('should get an array of a conversation', function () use ($chatGPT, $conversationId, $parentId) { 27 | $return = $chatGPT->getConversationMessages($conversationId); 28 | $this->assertIsArray($return); 29 | })->group('working'); 30 | 31 | it('should auto generate conversation title', function () use ($chatGPT, $conversationId, $parentId) { 32 | $return = $chatGPT->generateConversationTitle($conversationId, $parentId); 33 | $this->assertTrue($return); 34 | })->group('working'); 35 | 36 | it('should setting conversation title', function () use ($chatGPT, $conversationId, $parentId) { 37 | $return = $chatGPT->updateConversationTitle($conversationId, 'test'); 38 | $this->assertTrue($return); 39 | })->group('working'); 40 | 41 | it('should delete conversation', function () use ($chatGPT, $conversationId, $parentId) { 42 | $return = $chatGPT->deleteConversation($conversationId); 43 | $this->assertTrue($return); 44 | })->group('working'); 45 | 46 | it('should delete conversations', function () use ($chatGPT) { 47 | $return = $chatGPT->clearConversations(); 48 | $this->assertTrue($return); 49 | })->group('working'); 50 | 51 | it('should return plugins list', function () use ($chatGPT) { 52 | $return = $chatGPT->getPlugins(); 53 | $this->assertIsArray($return); 54 | })->group('working'); 55 | 56 | it('should change history and training status', function () use ($chatGPT) { 57 | $return = $chatGPT->setChatHistoryAndTraining(true); 58 | $this->assertTrue($return); 59 | })->group('working'); 60 | -------------------------------------------------------------------------------- /tests/V2Test.php: -------------------------------------------------------------------------------- 1 | addMessage( 8 | 'You are ChatGPT, a large language model trained by OpenAI. Answer as concisely as possible.', 9 | 'system' 10 | ); 11 | 12 | it('should get a new conversation', function () use ($chatGPT) { 13 | $return = $chatGPT->ask('Hello, how are you?'); 14 | foreach ($return as $answer) { 15 | $this->assertArrayHasKey('answer', $answer); 16 | } 17 | })->group('working'); 18 | 19 | it('should get a answer contact the context', function () use ($chatGPT) { 20 | $chatGPT->ask('Hello, how are you?'); 21 | $return = $chatGPT->ask('What did I ask you just now?'); 22 | foreach ($return as $answer) { 23 | $this->assertArrayHasKey('answer', $answer); 24 | $this->assertStringContainsString('Hello, how are you?', $answer['answer']); 25 | } 26 | })->group('working'); 27 | --------------------------------------------------------------------------------