|
| 1 | +<?php |
| 2 | + |
| 3 | +declare(strict_types=1); |
| 4 | + |
| 5 | +/** |
| 6 | + * This file is part of the guanguans/ai-commit. |
| 7 | + * |
| 8 | + * (c) guanguans <ityaozm@gmail.com> |
| 9 | + * |
| 10 | + * This source file is subject to the MIT license that is bundled. |
| 11 | + */ |
| 12 | + |
| 13 | +namespace App\Support; |
| 14 | + |
| 15 | +use GuzzleHttp\Middleware; |
| 16 | +use GuzzleHttp\Psr7\Utils; |
| 17 | +use Illuminate\Contracts\Container\BindingResolutionException; |
| 18 | +use Illuminate\Http\Client\PendingRequest; |
| 19 | +use Illuminate\Http\Client\RequestException; |
| 20 | +use Illuminate\Http\Client\Response; |
| 21 | +use Psr\Http\Message\ResponseInterface; |
| 22 | + |
| 23 | +/** |
| 24 | + * @see https://platform.moonshot.cn/docs/api-reference |
| 25 | + */ |
| 26 | +final class Kimi extends FoundationSDK |
| 27 | +{ |
| 28 | + /** |
| 29 | + * ```ok |
| 30 | + * { |
| 31 | + * 'id': 'chatcmpl-6pqDoRwRGQAlRvJnesR9QMG9rxpyK', |
| 32 | + * 'object': 'chat.completion', |
| 33 | + * 'created': 1677813488, |
| 34 | + * 'model': 'gpt-3.5-turbo-0301', |
| 35 | + * 'usage': { |
| 36 | + * 'prompt_tokens': 8, |
| 37 | + * 'completion_tokens': 16, |
| 38 | + * 'total_tokens': 24 |
| 39 | + * }, |
| 40 | + * 'choices': [ |
| 41 | + * { |
| 42 | + * 'delta': { |
| 43 | + * 'role': 'assistant', |
| 44 | + * 'content': 'PHP (Hypertext Preprocessor) is a server-side scripting language used' |
| 45 | + * }, |
| 46 | + * 'finish_reason': 'length', |
| 47 | + * 'index': 0 |
| 48 | + * } |
| 49 | + * ] |
| 50 | + * } |
| 51 | + * ```. |
| 52 | + * |
| 53 | + * ```stream ok |
| 54 | + * data: {"id":"chatcmpl-6pqQB0NVBCjNcs6aPeFUi4gy1pCoj","object":"chat.completion.chunk","created":1677814255,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{"content":" used"},"index":0,"finish_reason":null}]} |
| 55 | + * |
| 56 | + * data: {"id":"chatcmpl-6pqQB0NVBCjNcs6aPeFUi4gy1pCoj","object":"chat.completion.chunk","created":1677814255,"model":"gpt-3.5-turbo-0301","choices":[{"delta":{},"index":0,"finish_reason":"length"}]} |
| 57 | + * |
| 58 | + * data: [DONE] |
| 59 | + * ``` |
| 60 | + * |
| 61 | + * @psalm-suppress UnusedVariable |
| 62 | + * @psalm-suppress UnevaluatedCode |
| 63 | + * |
| 64 | + * @throws BindingResolutionException |
| 65 | + * @throws RequestException |
| 66 | + */ |
| 67 | + public function chatCompletions(array $parameters, ?callable $writer = null): Response |
| 68 | + { |
| 69 | + $response = $this |
| 70 | + ->cloneDefaultPendingRequest() |
| 71 | + ->when( |
| 72 | + ($parameters['stream'] ?? false) && \is_callable($writer), |
| 73 | + static function (PendingRequest $pendingRequest) use (&$rowData, $writer): PendingRequest { |
| 74 | + return $pendingRequest->withOptions([ |
| 75 | + 'curl' => [ |
| 76 | + CURLOPT_WRITEFUNCTION => static function ($ch, string $data) use (&$rowData, $writer): int { |
| 77 | + // $sanitizeData = self::sanitizeData($data); |
| 78 | + // if (! str($data)->startsWith('data: [DONE]')) { |
| 79 | + // $rowData = $sanitizeData; |
| 80 | + // } |
| 81 | + |
| 82 | + $rowData .= $data; |
| 83 | + |
| 84 | + $writer($data, $ch); |
| 85 | + |
| 86 | + return \strlen($data); |
| 87 | + }, |
| 88 | + ], |
| 89 | + ]); |
| 90 | + } |
| 91 | + ) |
| 92 | + // ->withMiddleware( |
| 93 | + // Middleware::mapResponse(static function (ResponseInterface $response): ResponseInterface { |
| 94 | + // $contents = $response->getBody()->getContents(); |
| 95 | + // |
| 96 | + // // $parameters['stream'] === true && $writer === null |
| 97 | + // if ($contents && ! \str($contents)->jsonValidate()) { |
| 98 | + // $data = \str($contents) |
| 99 | + // ->explode("\n\n") |
| 100 | + // ->reverse() |
| 101 | + // ->skip(2) |
| 102 | + // ->reverse() |
| 103 | + // ->map(static function (string $rowData): array { |
| 104 | + // return json_decode(self::sanitizeData($rowData), true); |
| 105 | + // }) |
| 106 | + // ->reduce(static function (array $data, array $rowData): array { |
| 107 | + // if (empty($data)) { |
| 108 | + // return $rowData; |
| 109 | + // } |
| 110 | + // |
| 111 | + // foreach ($data['choices'] as $index => $choice) { |
| 112 | + // $data['choices'][$index]['text'] .= $rowData['choices'][$index]['text']; |
| 113 | + // } |
| 114 | + // |
| 115 | + // return $data; |
| 116 | + // }, []); |
| 117 | + // |
| 118 | + // return $response->withBody(Utils::streamFor(json_encode($data))); |
| 119 | + // } |
| 120 | + // |
| 121 | + // return $response; |
| 122 | + // }) |
| 123 | + // ) |
| 124 | + ->post('chat/completions', validate($parameters, [ |
| 125 | + 'model' => [ |
| 126 | + 'required', |
| 127 | + 'string', |
| 128 | + 'in:moonshot-v1-8k,moonshot-v1-32k,moonshot-v1-128k', |
| 129 | + ], |
| 130 | + 'messages' => 'required|array', |
| 131 | + 'temperature' => 'numeric|between:0,2', |
| 132 | + 'top_p' => 'numeric|between:0,1', |
| 133 | + 'n' => 'integer|min:1', |
| 134 | + 'stream' => 'bool', |
| 135 | + // 'stop' => 'nullable|string|array', |
| 136 | + 'stop' => 'nullable|string', |
| 137 | + 'max_tokens' => 'integer', |
| 138 | + 'presence_penalty' => 'numeric|between:-2,2', |
| 139 | + 'frequency_penalty' => 'numeric|between:-2,2', |
| 140 | + 'logit_bias' => 'array', // map |
| 141 | + 'user' => 'string|uuid', |
| 142 | + ])) |
| 143 | + // ->onError(function (Response $response) use ($rowData) { |
| 144 | + // if ($rowData && empty($response->body())) { |
| 145 | + // (function (Response $response) use ($rowData): void { |
| 146 | + // $this->response = $response->toPsrResponse()->withBody( |
| 147 | + // Utils::streamFor(OpenAI::sanitizeData($rowData)) |
| 148 | + // ); |
| 149 | + // })->call($response, $response); |
| 150 | + // } |
| 151 | + // }) |
| 152 | +; |
| 153 | + |
| 154 | + if ($rowData && empty($response->body())) { |
| 155 | + $response = new Response($response->toPsrResponse()->withBody(Utils::streamFor(($rowData)))); |
| 156 | + } |
| 157 | + |
| 158 | + return $response->throw(); |
| 159 | + } |
| 160 | + |
| 161 | + /** |
| 162 | + * @throws RequestException |
| 163 | + */ |
| 164 | + public function models(): Response |
| 165 | + { |
| 166 | + return $this->cloneDefaultPendingRequest()->get('models')->throw(); |
| 167 | + } |
| 168 | + |
| 169 | + /** |
| 170 | + * {@inheritDoc} |
| 171 | + * |
| 172 | + * @throws BindingResolutionException |
| 173 | + */ |
| 174 | + protected function validateConfig(array $config): array |
| 175 | + { |
| 176 | + return array_replace_recursive( |
| 177 | + [ |
| 178 | + 'http_options' => [ |
| 179 | + // \GuzzleHttp\RequestOptions::CONNECT_TIMEOUT => 30, |
| 180 | + // \GuzzleHttp\RequestOptions::TIMEOUT => 180, |
| 181 | + ], |
| 182 | + 'retry' => [ |
| 183 | + // 'times' => 1, |
| 184 | + // 'sleep' => 1000, |
| 185 | + // 'when' => static function (\Exception $exception): bool { |
| 186 | + // return $exception instanceof \Illuminate\Http\Client\ConnectionException; |
| 187 | + // }, |
| 188 | + // // 'throw' => true, |
| 189 | + ], |
| 190 | + 'base_url' => 'https://api.moonshot.cn/v1', |
| 191 | + ], |
| 192 | + validate($config, [ |
| 193 | + 'http_options' => 'array', |
| 194 | + 'retry' => 'array', |
| 195 | + 'retry.times' => 'integer', |
| 196 | + 'retry.sleep' => 'integer', |
| 197 | + 'retry.when' => 'nullable', |
| 198 | + // 'retry.throw' => 'bool', |
| 199 | + 'base_url' => 'string', |
| 200 | + 'api_key' => 'required|string', |
| 201 | + ]) |
| 202 | + ); |
| 203 | + } |
| 204 | + |
| 205 | + /** |
| 206 | + * {@inheritDoc} |
| 207 | + */ |
| 208 | + protected function buildDefaultPendingRequest(array $config): PendingRequest |
| 209 | + { |
| 210 | + return parent::buildDefaultPendingRequest($config) |
| 211 | + ->baseUrl($config['base_url']) |
| 212 | + ->asJson() |
| 213 | + ->withToken($config['api_key']) |
| 214 | + // ->dump() |
| 215 | + // ->throw() |
| 216 | + // ->retry( |
| 217 | + // $config['retry']['times'], |
| 218 | + // $config['retry']['sleep'], |
| 219 | + // $config['retry']['when'] |
| 220 | + // // $config['retry']['throw'] |
| 221 | + // ) |
| 222 | + ->withOptions($config['http_options']); |
| 223 | + } |
| 224 | +} |
0 commit comments