├── .gitattributes ├── examples ├── aws │ └── vision_test.jpeg ├── mapper │ ├── model-mapper.php │ ├── model-mapper-stream.php │ ├── vision.php │ ├── vision_stream.php │ ├── vision_base64.php │ └── vision_stream_base64.php ├── qianfan_embeddings.php ├── chat_o3.php ├── chat_doubao.php ├── stream.php ├── chat.php ├── chat_with_http_mcp.php ├── chat_with_stdio_mcp.php └── exception │ └── oversize_image_error_example.php ├── .phpstorm.meta.php ├── src ├── Prompt │ ├── DefaultSystemMessage.prompt │ ├── KnowledgeAutoQA.prompt │ ├── code-interpreter.prompt │ ├── AbstractPromptTemplate.php │ ├── AfterCodeExecuted.prompt │ ├── PromptInterface.php │ ├── DataAnalyzePromptTemplate.php │ ├── CodeInterpreter.prompt │ ├── Prompt.php │ └── OpenAIToolsAgentPrompt.php ├── Api │ ├── Providers │ │ ├── AbstractApi.php │ │ ├── AwsBedrock │ │ │ ├── Cache │ │ │ │ ├── Strategy │ │ │ │ │ ├── CacheStrategyInterface.php │ │ │ │ │ ├── NoneCacheStrategy.php │ │ │ │ │ └── CachePointMessage.php │ │ │ │ └── AutoCacheConfig.php │ │ │ ├── AwsType.php │ │ │ ├── ConverterInterface.php │ │ │ ├── MergedToolMessage.php │ │ │ ├── AwsBedrockConfig.php │ │ │ └── AwsBedrock.php │ │ ├── DashScope │ │ │ ├── Cache │ │ │ │ ├── Strategy │ │ │ │ │ ├── DashScopeCacheStrategyInterface.php │ │ │ │ │ ├── AutoCacheStrategy.php │ │ │ │ │ └── ManualCacheStrategy.php │ │ │ │ ├── DashScopeAutoCacheConfig.php │ │ │ │ └── DashScopeCachePointManager.php │ │ │ ├── DashScope.php │ │ │ └── DashScopeConfig.php │ │ ├── Gemini │ │ │ ├── Cache │ │ │ │ └── Strategy │ │ │ │ │ ├── CacheStrategyInterface.php │ │ │ │ │ ├── CachePointMessage.php │ │ │ │ │ └── GeminiMessageCacheManager.php │ │ │ └── Gemini.php │ │ ├── OpenAI │ │ │ ├── OpenAI.php │ │ │ ├── Client.php │ │ │ └── OpenAIConfig.php │ │ └── AzureOpenAI │ │ │ ├── AzureOpenAI.php │ │ │ ├── AzureOpenAIConfig.php │ │ │ └── Client.php │ └── Response │ │ ├── TextCompletionChoice.php │ │ ├── Embedding.php │ │ ├── ListResponse.php │ │ ├── Model.php │ │ └── AbstractResponse.php ├── Exception │ ├── RuntimeException.php │ ├── InvalidArgumentException.php │ ├── McpException.php │ ├── LLMException.php │ ├── LLMException │ │ ├── Network │ │ │ ├── LLMThinkingStreamTimeoutException.php │ │ │ ├── LLMReadTimeoutException.php │ │ │ ├── LLMConnectionTimeoutException.php │ │ │ └── LLMStreamTimeoutException.php │ │ ├── Model │ │ │ ├── LLMModalityNotSupportedException.php │ │ │ ├── LLMFunctionCallNotSupportedException.php │ │ │ ├── LLMEmbeddingNotSupportedException.php │ │ │ ├── LLMContentFilterException.php │ │ │ ├── LLMImageUrlAccessException.php │ │ │ └── LLMContextLengthException.php │ │ ├── LLMNetworkException.php │ │ ├── LLMApiException.php │ │ ├── Configuration │ │ │ ├── LLMInvalidApiKeyException.php │ │ │ └── LLMInvalidEndpointException.php │ │ ├── LLMConfigurationException.php │ │ ├── LLMModelException.php │ │ ├── ErrorHandlerInterface.php │ │ └── Api │ │ │ └── LLMRateLimitException.php │ ├── ToolParameterValidationException.php │ └── OdinException.php ├── Mcp │ └── McpType.php ├── Message │ ├── Role.php │ ├── CachePoint.php │ ├── SystemMessage.php │ └── UserMessageContent.php ├── Contract │ ├── Api │ │ ├── Response │ │ │ └── ResponseInterface.php │ │ ├── Request │ │ │ └── RequestInterface.php │ │ ├── ConfigInterface.php │ │ └── ClientInterface.php │ ├── Tool │ │ └── ToolInterface.php │ ├── Mcp │ │ ├── McpServerManagerInterface.php │ │ └── McpServerConfigInterface.php │ ├── Model │ │ └── EmbeddingInterface.php │ ├── Memory │ │ ├── PolicyInterface.php │ │ ├── DriverInterface.php │ │ └── MemoryInterface.php │ └── Message │ │ └── MessageInterface.php ├── Model │ ├── Embedding.php │ ├── AwsBedrockModel.php │ ├── AzureOpenAIModel.php │ ├── RWKVModel.php │ ├── GeminiModel.php │ ├── OllamaModel.php │ ├── ChatglmModel.php │ ├── DoubaoModel.php │ ├── OpenAIModel.php │ ├── QianFanModel.php │ └── DashScopeModel.php ├── Constants │ └── ModelType.php ├── Document │ ├── MarkdownDocument.php │ └── Document.php ├── Utils │ ├── EventUtil.php │ ├── ModelUtil.php │ ├── TimeUtil.php │ ├── VisionMessageValidator.php │ ├── MessageUtil.php │ └── ToolUtil.php ├── VectorStore │ └── Qdrant │ │ ├── QdrantFactory.php │ │ └── Config.php ├── ClassMap │ └── GuzzleHttp │ │ └── BodySummarizer.php ├── Event │ ├── AfterChatCompletionsStreamEvent.php │ ├── AfterEmbeddingsEvent.php │ └── EventCallbackListener.php ├── Loader │ └── Loader.php ├── TextSplitter │ └── CharacterTextSplitter.php ├── Memory │ ├── Policy │ │ ├── TimeWindowPolicy.php │ │ ├── RelevancyPolicy.php │ │ ├── SummarizationPolicy.php │ │ ├── CompositePolicy.php │ │ ├── AbstractPolicy.php │ │ └── TokenLimitPolicy.php │ └── PolicyRegistry.php ├── ConfigProvider.php ├── Agent │ └── Tool │ │ ├── UsedTool.php │ │ └── MultiToolUseParallelTool.php ├── Tool │ └── Definition │ │ └── Schema │ │ └── JsonSchemaValidator.php ├── Logger.php └── Factory │ └── ModelFactory.php ├── .gitignore ├── phpunit.xml ├── LICENSE ├── data └── response.txt ├── doc └── user-guide-cn │ ├── README.md │ └── 00-introduction.md ├── README-CN.md └── composer.json /.gitattributes: -------------------------------------------------------------------------------- 1 | /tests export-ignore 2 | /.github export-ignore 3 | -------------------------------------------------------------------------------- /examples/aws/vision_test.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hyperf/odin/HEAD/examples/aws/vision_test.jpeg -------------------------------------------------------------------------------- /.phpstorm.meta.php: -------------------------------------------------------------------------------- 1 | embeddings; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | ./tests/ 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/Contract/Tool/ToolInterface.php: -------------------------------------------------------------------------------- 1 | splitText($this->getContent()); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Exception/InvalidArgumentException.php: -------------------------------------------------------------------------------- 1 | 23 | */ 24 | public function getAllTools(): array; 25 | 26 | public function callMcpTool(string $toolName, array $args = []): array; 27 | } 28 | -------------------------------------------------------------------------------- /src/Exception/McpException.php: -------------------------------------------------------------------------------- 1 | $value) { 21 | $dataStr .= $key . ' => ' . $value . PHP_EOL; 22 | } 23 | return <<has(EventDispatcherInterface::class)) { 24 | return; 25 | } 26 | $dispatcher = ApplicationContext::getContainer()->get(EventDispatcherInterface::class); 27 | $dispatcher->dispatch($event); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/VectorStore/Qdrant/QdrantFactory.php: -------------------------------------------------------------------------------- 1 | truncateAt = $truncateAt; 24 | } 25 | 26 | /** 27 | * Returns a summarized message body. 28 | */ 29 | public function summarize(MessageInterface $message): ?string 30 | { 31 | return Psr7\Message::bodySummary($message, $this->truncateAt); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Contract/Memory/PolicyInterface.php: -------------------------------------------------------------------------------- 1 | scheme; 28 | } 29 | 30 | public function getHost(): string 31 | { 32 | return $this->host; 33 | } 34 | 35 | public function getPort(): int 36 | { 37 | return $this->port; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Event/AfterChatCompletionsStreamEvent.php: -------------------------------------------------------------------------------- 1 | firstResponseDuration = $firstResponseDuration; 26 | parent::__construct($completionRequest, null, 0); 27 | } 28 | 29 | public function getFirstResponseDuration(): float 30 | { 31 | return $this->firstResponseDuration; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Loader/Loader.php: -------------------------------------------------------------------------------- 1 | $fileInfo['filename'], 25 | 'file_extension' => $fileInfo['extension'], 26 | 'file_hash' => md5($fileInfo['basename'] . $content), 27 | 'create_time' => $currentTime = date('Y-m-d H:i:s'), 28 | 'update_time' => $currentTime, 29 | ]); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/Message/CachePoint.php: -------------------------------------------------------------------------------- 1 | type = $type; 30 | } 31 | 32 | public function getType(): string 33 | { 34 | return $this->type; 35 | } 36 | 37 | public function toArray(): array 38 | { 39 | return [ 40 | 'type' => $this->type, 41 | ]; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Exception/LLMException/Network/LLMThinkingStreamTimeoutException.php: -------------------------------------------------------------------------------- 1 | $this->role->value, 44 | 'content' => $this->content, 45 | ]; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Exception/LLMException/Model/LLMModalityNotSupportedException.php: -------------------------------------------------------------------------------- 1 | separator = $separator; 24 | $this->isSeparatorRegex = $isSeparatorRegex; 25 | } 26 | 27 | public function splitText(string $text): array|bool 28 | { 29 | $separator = $this->isSeparatorRegex ? $this->separator : preg_quote($this->separator, '/'); 30 | $splits = preg_split("/({$separator})/", $text, -1, PREG_SPLIT_DELIM_CAPTURE); 31 | return array_filter($splits, function ($value) { 32 | return $value !== ''; 33 | }); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/Exception/LLMException/LLMNetworkException.php: -------------------------------------------------------------------------------- 1 | 60, // 默认时间窗口为60分钟 45 | ]; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Exception/LLMException/LLMApiException.php: -------------------------------------------------------------------------------- 1 | $tools 33 | */ 34 | public function convertTools(array $tools, bool $cache = false): array; 35 | } 36 | -------------------------------------------------------------------------------- /src/Api/Providers/Gemini/Cache/Strategy/CacheStrategyInterface.php: -------------------------------------------------------------------------------- 1 | config; 30 | 31 | // 使用ClientFactory创建AWS Bedrock客户端 32 | return ClientFactory::createAwsBedrockClient( 33 | $config, 34 | $this->getApiRequestOptions(), 35 | $this->logger 36 | ); 37 | } 38 | 39 | /** 40 | * 获取API版本路径. 41 | * AWS Bedrock无需API版本路径. 42 | */ 43 | protected function getApiVersionPath(): string 44 | { 45 | return ''; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /data/response.txt: -------------------------------------------------------------------------------- 1 | Thought: The user wants to group the "建店信息表" (Store Information Table) by "大区" (Region). 2 | 3 | Action: ModifyViews 4 | 5 | Observation: I need to modify the view of the "建店信息表-主表" (Store Information Table - Main Table) to include the "大区" (Region) column in the groups section. 6 | 7 | Action: 8 | 9 | ```json 10 | { 11 | "action": "modify_views", 12 | "original_views": [ 13 | { 14 | "view_type": "table", 15 | "name": "建店信息表-主表" 16 | } 17 | ], 18 | "new_views": [ 19 | { 20 | "view_type": "table", 21 | "table_name": "建店信息表", 22 | "name": "建店信息表-主表", 23 | "functions": { 24 | "filters": [], 25 | "sorts": [], 26 | "groups": [ 27 | { 28 | "column": "大区" 29 | } 30 | ] 31 | }, 32 | "columns": [] 33 | } 34 | ] 35 | } 36 | ``` 37 | 38 | Thought: The view has been modified to group the "建店信息表" (Store Information Table) by "大区" (Region). 39 | 40 | Final Answer: "建店信息表" (Store Information Table) has been grouped by "大区" (Region). -------------------------------------------------------------------------------- /src/ConfigProvider.php: -------------------------------------------------------------------------------- 1 | [ 25 | [ 26 | 'id' => 'config', 27 | 'description' => 'The config for odin.', 28 | 'source' => __DIR__ . '/../publish/odin.php', 29 | 'destination' => BASE_PATH . '/config/autoload/odin.php', 30 | ], 31 | ], 32 | 'dependencies' => [ 33 | Qdrant::class => QdrantFactory::class, 34 | ], 35 | 'listeners' => [ 36 | EventCallbackListener::class, 37 | ], 38 | ]; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Contract/Mcp/McpServerConfigInterface.php: -------------------------------------------------------------------------------- 1 | 0.7, // 最小相关性分数 46 | 'max_messages' => 10, // 最多保留消息数 47 | ]; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/Api/Response/TextCompletionChoice.php: -------------------------------------------------------------------------------- 1 | text; 32 | } 33 | 34 | public function getIndex(): ?int 35 | { 36 | return $this->index; 37 | } 38 | 39 | public function getLogprobs(): ?string 40 | { 41 | return $this->logprobs; 42 | } 43 | 44 | public function getFinishReason(): ?string 45 | { 46 | return $this->finishReason; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Contract/Memory/DriverInterface.php: -------------------------------------------------------------------------------- 1 | 15, // 触发摘要的消息数量阈值 46 | 'keep_recent' => 5, // 保留最近的消息数量 47 | 'summary_prompt' => '请总结以下对话内容,提取关键信息:', // 摘要提示词 48 | ]; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Model/AzureOpenAIModel.php: -------------------------------------------------------------------------------- 1 | 'max_completion_tokens', 27 | ]; 28 | 29 | /** 30 | * 获取Azure OpenAI客户端实例. 31 | */ 32 | protected function getClient(): ClientInterface 33 | { 34 | // Azure OpenAI通过Client自己处理URL路径,不需要使用processApiBaseUrl 35 | // 因为它的URL结构比较特殊: {endpoint}/openai/deployments/{deployment-id}/chat/completions?api-version={api-version} 36 | 37 | // 使用ClientFactory创建AzureOpenAI客户端 38 | return ClientFactory::createAzureOpenAIClient( 39 | $this->config, 40 | $this->getApiRequestOptions(), 41 | $this->logger 42 | ); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Model/RWKVModel.php: -------------------------------------------------------------------------------- 1 | config; 31 | $this->processApiBaseUrl($config); 32 | 33 | $openAI = new OpenAI(); 34 | $config = new OpenAIConfig( 35 | apiKey: $config['api_key'] ?? '', 36 | organization: '', // RWKV不需要组织ID 37 | baseUrl: $config['base_url'] ?? 'http://localhost:8000' 38 | ); 39 | return $openAI->getClient($config, $this->getApiRequestOptions(), $this->logger); 40 | } 41 | 42 | protected function getApiVersionPath(): string 43 | { 44 | return 'v1'; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Model/GeminiModel.php: -------------------------------------------------------------------------------- 1 | config; 29 | $this->processApiBaseUrl($config); 30 | 31 | // Use ClientFactory to create Gemini client 32 | return ClientFactory::createClient( 33 | 'gemini', 34 | $config, 35 | $this->getApiRequestOptions(), 36 | $this->logger 37 | ); 38 | } 39 | 40 | /** 41 | * Get API version path 42 | * Gemini uses OpenAI-compatible API, so no version path is needed. 43 | */ 44 | protected function getApiVersionPath(): string 45 | { 46 | return ''; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Exception/ToolParameterValidationException.php: -------------------------------------------------------------------------------- 1 | validationErrors = $validationErrors; 42 | parent::__construct($message, $code, $previous, $code ?: 4001, 400); 43 | } 44 | 45 | /** 46 | * 获取验证错误信息. 47 | */ 48 | public function getValidationErrors(): array 49 | { 50 | return $this->validationErrors; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Api/Response/Embedding.php: -------------------------------------------------------------------------------- 1 | embedding; 27 | } 28 | 29 | public function setEmbedding(array $embedding): self 30 | { 31 | $this->embedding = $embedding; 32 | return $this; 33 | } 34 | 35 | public function getIndex(): int 36 | { 37 | return $this->index; 38 | } 39 | 40 | public function setIndex(int $index): self 41 | { 42 | $this->index = $index; 43 | return $this; 44 | } 45 | 46 | public function toArray(): array 47 | { 48 | return [ 49 | 'object' => 'embedding', 50 | 'embedding' => $this->embedding, 51 | 'index' => $this->index, 52 | ]; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Exception/LLMException/Model/LLMEmbeddingNotSupportedException.php: -------------------------------------------------------------------------------- 1 | model; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Model/OllamaModel.php: -------------------------------------------------------------------------------- 1 | config; 31 | $this->processApiBaseUrl($config); 32 | 33 | $openAI = new OpenAI(); 34 | $config = new OpenAIConfig( 35 | apiKey: $config['api_key'] ?? '', // Ollama不需要API Key 36 | organization: '', // Ollama不需要组织ID 37 | baseUrl: $config['base_url'] ?? 'http://0.0.0.0:11434', 38 | skipApiKeyValidation: true, // 显式标记Ollama不需要API Key验证 39 | ); 40 | return $openAI->getClient($config, $this->getApiRequestOptions(), $this->logger); 41 | } 42 | 43 | protected function getApiVersionPath(): string 44 | { 45 | return 'v1'; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/Model/ChatglmModel.php: -------------------------------------------------------------------------------- 1 | config; 31 | $this->processApiBaseUrl($config); 32 | 33 | $openAI = new OpenAI(); 34 | $config = new OpenAIConfig( 35 | apiKey: $config['api_key'] ?? '', 36 | organization: '', // Chatglm不需要组织ID 37 | baseUrl: $config['base_url'] ?? 'http://localhost:8000' 38 | ); 39 | return $openAI->getClient($config, $this->getApiRequestOptions(), $this->logger); 40 | } 41 | 42 | /** 43 | * 获取API版本路径. 44 | * ChatGLM的API版本路径为 api/paas/v4. 45 | */ 46 | protected function getApiVersionPath(): string 47 | { 48 | return 'api/paas/v4'; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Exception/LLMException/LLMModelException.php: -------------------------------------------------------------------------------- 1 | model = $model; 46 | } 47 | 48 | /** 49 | * 获取模型名称. 50 | */ 51 | public function getModel(): ?string 52 | { 53 | return $this->model; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /doc/user-guide-cn/README.md: -------------------------------------------------------------------------------- 1 | # Odin 使用手册 2 | 3 | 欢迎使用 Odin PHP LLM 开发框架!本手册将帮助您快速上手并充分利用 Odin 的全部功能。 4 | 5 | ## 目录 6 | 7 | 1. [简介](./00-introduction.md) 8 | - 什么是Odin 9 | - 设计理念 10 | - 框架架构 11 | - 核心概念和术语 12 | 13 | 2. [安装和配置](./01-installation.md) 14 | - 系统要求 15 | - 安装步骤 16 | - 初始配置 17 | - 环境变量 18 | - 配置文件 19 | 20 | 3. [核心概念](./02-core-concepts.md) 21 | - LLM模型接口 22 | - 消息和会话 23 | - 工具调用 24 | - 记忆管理 25 | - 向量存储 26 | - 异常处理 27 | 28 | 4. [API参考](./03-api-reference.md) 29 | - 类和接口说明 30 | - 方法参数和返回值 31 | - 示例代码 32 | 33 | 5. [模型提供商](./04-model-providers.md) 34 | - 支持的模型 35 | - 提供商特性对比 36 | - 配置不同提供商 37 | - 添加新的提供商 38 | 39 | 6. [工具开发](./05-tool-development.md) 40 | - 工具概念和用途 41 | - 内置工具 42 | - 自定义工具开发 43 | - 工具测试 44 | 45 | 7. [记忆管理](./06-memory-management.md) 46 | - 记忆策略 47 | - 记忆管理器 48 | - 会话记忆示例 49 | - 自定义记忆实现 50 | 51 | 8. [Agent开发](./07-agent-development.md) 52 | - Agent架构 53 | - 工具规划和执行 54 | - 状态管理 55 | - 复杂Agent示例 56 | 57 | 9. [测试和调试](./08-testing-debugging.md) 58 | - 单元测试 59 | - 日志记录和分析 60 | - 问题排查 61 | - 性能分析 62 | 63 | 10. [示例项目](./09-examples.md) 64 | - 聊天应用 65 | - 文档问答系统 66 | - 智能助手 67 | - 流程自动化 68 | 69 | 11. [MCP 集成](./11-mcp-integration.md) 70 | - MCP 协议概述 71 | - 服务器配置与连接 72 | - 工具发现与调用 73 | - 最佳实践与故障排查 74 | 75 | 12. [常见问题解答](./10-faq.md) 76 | - 常见错误 77 | - 性能问题 78 | - 兼容性问题 -------------------------------------------------------------------------------- /src/Model/DoubaoModel.php: -------------------------------------------------------------------------------- 1 | config; 33 | $this->processApiBaseUrl($config); 34 | 35 | $openAI = new OpenAI(); 36 | $config = new OpenAIConfig( 37 | apiKey: $config['api_key'] ?? '', 38 | organization: '', // Doubao不需要组织ID 39 | baseUrl: $config['base_url'] ?? '' 40 | ); 41 | return $openAI->getClient($config, $this->getApiRequestOptions(), $this->logger); 42 | } 43 | 44 | /** 45 | * 获取API版本路径. 46 | * Doubao的API版本路径为 api/v3. 47 | */ 48 | protected function getApiVersionPath(): string 49 | { 50 | return 'api/v3'; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Document/Document.php: -------------------------------------------------------------------------------- 1 | splitText($this->content); 25 | } 26 | 27 | public function getContent(): string 28 | { 29 | return $this->content; 30 | } 31 | 32 | public function setContent(string $content): self 33 | { 34 | $this->content = $content; 35 | return $this; 36 | } 37 | 38 | public function getMetadata(): array 39 | { 40 | return $this->metadata; 41 | } 42 | 43 | public function setMetadata(array $metadata): self 44 | { 45 | $this->metadata = $metadata; 46 | return $this; 47 | } 48 | 49 | public function appendMetadata(string $key, string $value): self 50 | { 51 | $this->metadata[$key] = $value; 52 | return $this; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Api/Providers/Gemini/Cache/Strategy/CachePointMessage.php: -------------------------------------------------------------------------------- 1 | originMessage = $originMessage; 28 | $this->tokens = $tokens; 29 | $this->getHash(); 30 | } 31 | 32 | public function getOriginMessage(): mixed 33 | { 34 | return $this->originMessage; 35 | } 36 | 37 | public function getHash(): string 38 | { 39 | if (! empty($this->hash)) { 40 | return $this->hash; 41 | } 42 | 43 | if ($this->originMessage instanceof MessageInterface) { 44 | $this->hash = $this->originMessage->getHash(); 45 | } else { 46 | $this->hash = md5(serialize($this->originMessage)); 47 | } 48 | return $this->hash; 49 | } 50 | 51 | public function getTokens(): int 52 | { 53 | return $this->tokens; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Exception/LLMException/ErrorHandlerInterface.php: -------------------------------------------------------------------------------- 1 | getContents(); 36 | 37 | // No contents to validate 38 | if (empty($contents)) { 39 | return; 40 | } 41 | 42 | foreach ($contents as $content) { 43 | if ($content->getType() === 'image_url') { 44 | $imageUrl = $content->getImageUrl(); 45 | if (! empty($imageUrl)) { 46 | ImageFormatValidator::validateImageUrl($imageUrl); 47 | } 48 | } 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Api/Providers/AwsBedrock/Cache/Strategy/CachePointMessage.php: -------------------------------------------------------------------------------- 1 | originMessage = $originMessage; 28 | $this->tokens = $tokens; 29 | $this->getHash(); 30 | } 31 | 32 | public function getOriginMessage(): mixed 33 | { 34 | return $this->originMessage; 35 | } 36 | 37 | public function getHash(): string 38 | { 39 | if (! empty($this->hash)) { 40 | return $this->hash; 41 | } 42 | 43 | if ($this->originMessage instanceof MessageInterface) { 44 | $this->hash = $this->originMessage->getHash(); 45 | } else { 46 | $this->hash = md5(serialize($this->originMessage)); 47 | } 48 | return $this->hash; 49 | } 50 | 51 | public function getTokens(): int 52 | { 53 | return $this->tokens; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Exception/LLMException/Configuration/LLMInvalidEndpointException.php: -------------------------------------------------------------------------------- 1 | endpoint = $endpoint; 40 | 41 | if ($endpoint) { 42 | $message = sprintf('%s: %s', $message, $endpoint); 43 | } 44 | 45 | parent::__construct($message, self::ERROR_CODE, $previous, 0, $statusCode); 46 | } 47 | 48 | /** 49 | * 获取终端点URL. 50 | */ 51 | public function getEndpoint(): ?string 52 | { 53 | return $this->endpoint; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/mapper/model-mapper.php: -------------------------------------------------------------------------------- 1 | get(ModelMapper::class); 29 | 30 | $modelId = \Hyperf\Support\env('MODEL_MAPPER_TEST_MODEL_ID', ''); 31 | 32 | $model = $modelMapper->getModel($modelId); 33 | 34 | $messages = [ 35 | new SystemMessage(''), 36 | new UserMessage('你好,你是谁'), 37 | ]; 38 | 39 | // 使用非流式API调用 40 | $start = microtime(true); 41 | $response = $model->chat($messages); 42 | $message = $response->getFirstChoice()->getMessage(); 43 | if ($message instanceof AssistantMessage) { 44 | echo $message->getReasoningContent() ?? $message->getContent(); 45 | } 46 | echo PHP_EOL; 47 | echo '非流式耗时' . (microtime(true) - $start) . '秒' . PHP_EOL; 48 | -------------------------------------------------------------------------------- /src/Exception/LLMException/Network/LLMReadTimeoutException.php: -------------------------------------------------------------------------------- 1 | timeoutSeconds = $timeoutSeconds; 40 | 41 | if ($timeoutSeconds !== null) { 42 | $message = sprintf('%s, timeout: %.2f seconds', $message, $timeoutSeconds); 43 | } 44 | 45 | parent::__construct($message, self::ERROR_CODE, $previous, 0, $statusCode); 46 | } 47 | 48 | /** 49 | * 获取超时时间(秒). 50 | */ 51 | public function getTimeoutSeconds(): ?float 52 | { 53 | return $this->timeoutSeconds; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Exception/LLMException/Api/LLMRateLimitException.php: -------------------------------------------------------------------------------- 1 | retryAfter = $retryAfter; 44 | 45 | if ($retryAfter !== null) { 46 | $message = sprintf('%s, retry after %d seconds', $message, $retryAfter); 47 | } 48 | 49 | parent::__construct($message, self::ERROR_CODE, $previous, 0, $statusCode); 50 | } 51 | 52 | /** 53 | * 获取建议的重试等待时间(秒). 54 | */ 55 | public function getRetryAfter(): ?int 56 | { 57 | return $this->retryAfter; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Exception/OdinException.php: -------------------------------------------------------------------------------- 1 | errorCode = $errorCode ?: $code; 40 | $this->statusCode = $statusCode; 41 | } 42 | 43 | /** 44 | * 获取HTTP状态码. 45 | */ 46 | public function getStatusCode(): int 47 | { 48 | return $this->statusCode; 49 | } 50 | 51 | /** 52 | * 设置HTTP状态码. 53 | */ 54 | public function setStatusCode(int $statusCode): self 55 | { 56 | $this->statusCode = $statusCode; 57 | return $this; 58 | } 59 | 60 | /** 61 | * 获取错误代码. 62 | */ 63 | public function getErrorCode(): int 64 | { 65 | return $this->errorCode; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Exception/LLMException/Network/LLMConnectionTimeoutException.php: -------------------------------------------------------------------------------- 1 | timeoutSeconds = $timeoutSeconds; 40 | 41 | if ($timeoutSeconds !== null) { 42 | $message = sprintf('%s, timeout: %.2f seconds', $message, $timeoutSeconds); 43 | } 44 | 45 | parent::__construct($message, self::ERROR_CODE, $previous, 0, $statusCode); 46 | } 47 | 48 | /** 49 | * 获取超时时间(秒). 50 | */ 51 | public function getTimeoutSeconds(): ?float 52 | { 53 | return $this->timeoutSeconds; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /examples/qianfan_embeddings.php: -------------------------------------------------------------------------------- 1 | env('QIANFAN_API_KEY'), 39 | 'base_url' => env('QIANFAN_BASE_URL'), 40 | ], 41 | modelOptions: ModelOptions::fromArray([ 42 | 'chat' => false, 43 | 'function_call' => false, 44 | 'embedding' => true, 45 | 'multi_modal' => true, 46 | 'vector_size' => 1024, 47 | ]), 48 | logger: $logger 49 | ); 50 | 51 | $data = $model->embeddings('量子纠缠的原理'); 52 | var_dump($data->getData()); 53 | -------------------------------------------------------------------------------- /src/Memory/Policy/CompositePolicy.php: -------------------------------------------------------------------------------- 1 | policies[] = $policy; 40 | return $this; 41 | } 42 | 43 | /** 44 | * 处理消息列表,按顺序应用所有策略. 45 | * 46 | * @param AbstractMessage[] $messages 原始消息列表 47 | * @return AbstractMessage[] 处理后的消息列表 48 | */ 49 | public function process(array $messages): array 50 | { 51 | $result = $messages; 52 | 53 | foreach ($this->policies as $policy) { 54 | $result = $policy->process($result); 55 | } 56 | 57 | return $result; 58 | } 59 | 60 | /** 61 | * 获取默认配置选项. 62 | * 63 | * @return array 默认配置选项 64 | */ 65 | protected function getDefaultOptions(): array 66 | { 67 | return []; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/Contract/Api/ClientInterface.php: -------------------------------------------------------------------------------- 1 | minCacheTokens = $minCacheTokens; 42 | $this->supportedModels = $supportedModels; 43 | $this->autoEnabled = $autoEnabled; 44 | } 45 | 46 | public function getMinCacheTokens(): int 47 | { 48 | return $this->minCacheTokens; 49 | } 50 | 51 | public function getSupportedModels(): array 52 | { 53 | return $this->supportedModels; 54 | } 55 | 56 | public function isAutoEnabled(): bool 57 | { 58 | return $this->autoEnabled; 59 | } 60 | 61 | public function isModelSupported(string $model): bool 62 | { 63 | return in_array($model, $this->supportedModels); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Event/AfterEmbeddingsEvent.php: -------------------------------------------------------------------------------- 1 | embeddingRequest = $embeddingRequest; 32 | $this->setEmbeddingResponse($embeddingResponse); 33 | $this->duration = $duration; 34 | } 35 | 36 | public function getEmbeddingRequest(): EmbeddingRequest 37 | { 38 | return $this->embeddingRequest; 39 | } 40 | 41 | public function getEmbeddingResponse(): EmbeddingResponse 42 | { 43 | return $this->embeddingResponse; 44 | } 45 | 46 | public function getDuration(): float 47 | { 48 | return $this->duration; 49 | } 50 | 51 | public function setEmbeddingResponse(EmbeddingResponse $embeddingResponse): void 52 | { 53 | $embeddingResponse = clone $embeddingResponse; 54 | $embeddingResponse->removeBigObject(); 55 | $this->embeddingResponse = $embeddingResponse; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/Exception/LLMException/Model/LLMContentFilterException.php: -------------------------------------------------------------------------------- 1 | contentLabels = $contentLabels; 45 | 46 | if (! empty($contentLabels)) { 47 | $labelsStr = implode(', ', $contentLabels); 48 | $message = sprintf('%s, reasons: %s', $message, $labelsStr); 49 | } 50 | 51 | parent::__construct($message, self::ERROR_CODE, $previous, 0, $model, $statusCode); 52 | } 53 | 54 | /** 55 | * 获取触发过滤的内容标签. 56 | */ 57 | public function getContentLabels(): ?array 58 | { 59 | return $this->contentLabels; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Api/Providers/DashScope/Cache/Strategy/AutoCacheStrategy.php: -------------------------------------------------------------------------------- 1 | isModelSupported($request->getModel())) { 29 | return; 30 | } 31 | 32 | // 2. 检查 token 数量 33 | $totalTokens = $request->getTotalTokenEstimate(); 34 | if ($totalTokens < $config->getMinCacheTokens()) { 35 | return; 36 | } 37 | 38 | // 3. 清除所有手动设置的缓存点,并为最后一条消息自动添加缓存点 39 | $messages = $request->getMessages(); 40 | if (! empty($messages)) { 41 | // 清除所有消息的手动缓存点 42 | foreach ($messages as $message) { 43 | $message->setCachePoint(null); 44 | } 45 | 46 | // 为最后一条消息设置自动缓存点 47 | $lastMessage = end($messages); 48 | $cachePoint = new CachePoint('ephemeral'); 49 | $lastMessage->setCachePoint($cachePoint); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/Exception/LLMException/Model/LLMImageUrlAccessException.php: -------------------------------------------------------------------------------- 1 | imageUrl = $imageUrl; 46 | 47 | if (! empty($imageUrl)) { 48 | $message = sprintf('%s, image URL: %s', $message, $imageUrl); 49 | } 50 | 51 | parent::__construct($message, self::ERROR_CODE, $previous, ErrorCode::MODEL_IMAGE_URL_ACCESS_ERROR, $model, $statusCode); 52 | } 53 | 54 | /** 55 | * 获取不可访问的图片URL. 56 | */ 57 | public function getImageUrl(): ?string 58 | { 59 | return $this->imageUrl; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Memory/PolicyRegistry.php: -------------------------------------------------------------------------------- 1 | 28 | */ 29 | private array $policies = []; 30 | 31 | /** 32 | * 注册策略. 33 | * 34 | * @param string $name 策略名称 35 | * @param PolicyInterface $policy 策略实例 36 | * @return self 支持链式调用 37 | */ 38 | public function register(string $name, PolicyInterface $policy): self 39 | { 40 | $this->policies[$name] = $policy; 41 | return $this; 42 | } 43 | 44 | /** 45 | * 获取策略. 46 | * 47 | * @param string $name 策略名称 48 | * @return null|PolicyInterface 策略实例或null 49 | */ 50 | public function get(string $name): ?PolicyInterface 51 | { 52 | return $this->policies[$name] ?? null; 53 | } 54 | 55 | /** 56 | * 策略是否存在. 57 | * 58 | * @param string $name 策略名称 59 | * @return bool 是否存在 60 | */ 61 | public function has(string $name): bool 62 | { 63 | return isset($this->policies[$name]); 64 | } 65 | 66 | /** 67 | * 获取所有策略. 68 | * 69 | * @return array 所有策略 70 | */ 71 | public function all(): array 72 | { 73 | return $this->policies; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /examples/mapper/model-mapper-stream.php: -------------------------------------------------------------------------------- 1 | get(ModelMapper::class); 31 | 32 | $modelId = \Hyperf\Support\env('MODEL_MAPPER_TEST_MODEL_ID', ''); 33 | 34 | $model = $modelMapper->getModel($modelId); 35 | 36 | $messages = [ 37 | new SystemMessage(''), 38 | new UserMessage('你好,你是谁'), 39 | ]; 40 | 41 | $response = $model->chatStream($messages); 42 | 43 | // 使用流式API调用 44 | $start = microtime(true); 45 | /** @var ChatCompletionChoice $choice */ 46 | foreach ($response->getStreamIterator() as $choice) { 47 | $message = $choice->getMessage(); 48 | if ($message instanceof AssistantMessage) { 49 | echo $message->getReasoningContent() ?? $message->getContent(); 50 | } 51 | } 52 | echo PHP_EOL; 53 | echo '流式耗时' . (microtime(true) - $start) . '秒' . PHP_EOL; 54 | -------------------------------------------------------------------------------- /src/Utils/MessageUtil.php: -------------------------------------------------------------------------------- 1 | $messages 26 | */ 27 | public static function filter(array $messages): array 28 | { 29 | $messagesArr = []; 30 | foreach ($messages as $message) { 31 | if ($message instanceof SystemMessage && $message->getContent() === '') { 32 | continue; 33 | } 34 | if ($message instanceof MessageInterface) { 35 | $messagesArr[] = $message->toArray(); 36 | } 37 | } 38 | return $messagesArr; 39 | } 40 | 41 | public static function createFromArray(array $message): ?MessageInterface 42 | { 43 | if (! isset($message['role'])) { 44 | return null; 45 | } 46 | return match ($message['role']) { 47 | 'assistant' => AssistantMessage::fromArray($message), 48 | 'system' => SystemMessage::fromArray($message), 49 | 'tool' => isset($message['tool_call_id']) ? ToolMessage::fromArray($message) : null, 50 | default => UserMessage::fromArray($message), 51 | }; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/Exception/LLMException/Network/LLMStreamTimeoutException.php: -------------------------------------------------------------------------------- 1 | timeoutType = $timeoutType; 45 | 46 | if ($timeoutSeconds !== null) { 47 | $message = sprintf('%s, timeout type: %s, waited: %.2f seconds', $message, $timeoutType, $timeoutSeconds); 48 | } else { 49 | $message = sprintf('%s, timeout type: %s', $message, $timeoutType); 50 | } 51 | 52 | parent::__construct($message, self::ERROR_CODE, $previous, 0, $statusCode); 53 | } 54 | 55 | /** 56 | * 获取超时类型. 57 | */ 58 | public function getTimeoutType(): string 59 | { 60 | return $this->timeoutType; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/Api/Providers/AwsBedrock/Cache/AutoCacheConfig.php: -------------------------------------------------------------------------------- 1 | maxCachePoints = $maxCachePoints; 46 | $this->minCacheTokens = $minCacheTokens; 47 | $this->refreshPointMinTokens = $refreshPointMinTokens; 48 | $this->minHitCount = $minHitCount; 49 | } 50 | 51 | public function getMaxCachePoints(): int 52 | { 53 | return $this->maxCachePoints; 54 | } 55 | 56 | public function getMinCacheTokens(): int 57 | { 58 | return $this->minCacheTokens; 59 | } 60 | 61 | public function getRefreshPointMinTokens(): int 62 | { 63 | return $this->refreshPointMinTokens; 64 | } 65 | 66 | public function getMinHitCount(): int 67 | { 68 | return $this->minHitCount; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /examples/mapper/vision.php: -------------------------------------------------------------------------------- 1 | get(ModelMapper::class); 35 | $model = $modelMapper->getModel($modelId); 36 | 37 | $userMessage = new UserMessage(); 38 | $userMessage->addContent(UserMessageContent::text('请分析下面图片中的内容,并描述其主要元素和可能的用途。')); 39 | $userMessage->addContent(UserMessageContent::imageUrl('https://tos-tools.tos-cn-beijing.volces.com/misc/sample1.jpg')); 40 | 41 | $start = microtime(true); 42 | 43 | // 使用非流式API调用 44 | $response = $model->chat([$userMessage]); 45 | 46 | // 输出完整响应 47 | $message = $response->getFirstChoice()->getMessage(); 48 | if ($message instanceof AssistantMessage) { 49 | echo $message->getReasoningContent() ?? $message->getContent(); 50 | } 51 | 52 | echo PHP_EOL; 53 | echo '耗时' . (microtime(true) - $start) . '秒' . PHP_EOL; 54 | -------------------------------------------------------------------------------- /src/Api/Providers/OpenAI/OpenAI.php: -------------------------------------------------------------------------------- 1 | getApiKey()) && ! $config->shouldSkipApiKeyValidation()) { 32 | throw new LLMInvalidApiKeyException('API密钥不能为空', null, 'OpenAI'); 33 | } 34 | 35 | if (empty($config->getBaseUrl())) { 36 | throw new LLMInvalidEndpointException('基础URL不能为空', null, $config->getBaseUrl()); 37 | } 38 | $requestOptions = $requestOptions ?? new ApiOptions(); 39 | 40 | $key = md5(json_encode($config->toArray()) . json_encode($requestOptions->toArray())); 41 | if (($this->clients[$key] ?? null) instanceof Client) { 42 | return $this->clients[$key]; 43 | } 44 | 45 | $client = new Client($config, $requestOptions, $logger); 46 | 47 | $this->clients[$key] = $client; 48 | return $this->clients[$key]; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Model/OpenAIModel.php: -------------------------------------------------------------------------------- 1 | config; 37 | $this->processApiBaseUrl($config); 38 | 39 | // 检查是否为qwen系列模型 40 | if (ModelUtil::isQwenModel($this->model)) { 41 | // 使用ClientFactory统一创建DashScope客户端 42 | return ClientFactory::createClient( 43 | 'dashscope', 44 | $config, 45 | $this->getApiRequestOptions(), 46 | $this->logger 47 | ); 48 | } 49 | 50 | // 使用ClientFactory统一创建OpenAI客户端 51 | return ClientFactory::createClient( 52 | 'openai', 53 | $config, 54 | $this->getApiRequestOptions(), 55 | $this->logger 56 | ); 57 | } 58 | 59 | /** 60 | * 获取API版本路径. 61 | * OpenAI的API版本路径为 v1. 62 | */ 63 | protected function getApiVersionPath(): string 64 | { 65 | return 'v1'; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/Api/Providers/Gemini/Gemini.php: -------------------------------------------------------------------------------- 1 | getApiKey()) && ! $config->shouldSkipApiKeyValidation()) { 32 | throw new LLMInvalidApiKeyException('API密钥不能为空', null, 'Gemini'); 33 | } 34 | 35 | if (empty($config->getBaseUrl())) { 36 | throw new LLMInvalidEndpointException('基础URL不能为空', null, $config->getBaseUrl()); 37 | } 38 | $requestOptions = $requestOptions ?? new ApiOptions(); 39 | 40 | $key = md5(json_encode($config->toArray()) . json_encode($requestOptions->toArray())); 41 | if (($this->clients[$key] ?? null) instanceof Client) { 42 | return $this->clients[$key]; 43 | } 44 | 45 | $client = new Client($config, $requestOptions, $logger); 46 | 47 | $this->clients[$key] = $client; 48 | return $this->clients[$key]; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /examples/chat_o3.php: -------------------------------------------------------------------------------- 1 | env('AZURE_OPENAI_O3_API_KEY'), 36 | 'api_base' => env('AZURE_OPENAI_O3_API_BASE'), 37 | 'api_version' => env('AZURE_OPENAI_O3_API_VERSION'), 38 | 'deployment_name' => env('AZURE_OPENAI_O3_DEPLOYMENT_NAME'), 39 | ], 40 | new Logger(), 41 | ); 42 | 43 | $messages = [ 44 | new SystemMessage(''), 45 | new UserMessage('你是谁'), 46 | ]; 47 | 48 | $start = microtime(true); 49 | 50 | // 使用非流式API调用 51 | $response = $model->chat($messages, temperature: 1, maxTokens: 2048); 52 | 53 | // 输出完整响应 54 | $message = $response->getFirstChoice()->getMessage(); 55 | if ($message instanceof AssistantMessage) { 56 | echo $message->getReasoningContent() ?? $message->getContent(); 57 | } 58 | 59 | echo PHP_EOL; 60 | echo '耗时' . (microtime(true) - $start) . '秒' . PHP_EOL; 61 | -------------------------------------------------------------------------------- /src/Agent/Tool/UsedTool.php: -------------------------------------------------------------------------------- 1 | $this->elapsedTime, 31 | 'success' => $this->success, 32 | 'id' => $this->id, 33 | 'name' => $this->name, 34 | 'arguments' => $this->arguments, 35 | 'result' => $this->result, 36 | 'error_message' => $this->errorMessage, 37 | ]; 38 | } 39 | 40 | public function getElapsedTime(): float 41 | { 42 | return $this->elapsedTime; 43 | } 44 | 45 | public function isSuccess(): bool 46 | { 47 | return $this->success; 48 | } 49 | 50 | public function getId(): string 51 | { 52 | return $this->id; 53 | } 54 | 55 | public function getName(): string 56 | { 57 | return $this->name; 58 | } 59 | 60 | public function getArguments(): array 61 | { 62 | return $this->arguments; 63 | } 64 | 65 | public function getResult(): mixed 66 | { 67 | return $this->result; 68 | } 69 | 70 | public function getErrorMessage(): string 71 | { 72 | return $this->errorMessage; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Api/Providers/DashScope/DashScope.php: -------------------------------------------------------------------------------- 1 | getApiKey()) && ! $config->shouldSkipApiKeyValidation()) { 35 | throw new LLMInvalidApiKeyException('DashScope API密钥不能为空', null, 'DashScope'); 36 | } 37 | 38 | if (empty($config->getBaseUrl())) { 39 | throw new LLMInvalidEndpointException('基础URL不能为空', null, $config->getBaseUrl()); 40 | } 41 | 42 | $requestOptions = $requestOptions ?? new ApiOptions(); 43 | 44 | $key = md5(json_encode($config->toArray()) . json_encode($requestOptions->toArray())); 45 | if (($this->clients[$key] ?? null) instanceof Client) { 46 | return $this->clients[$key]; 47 | } 48 | 49 | $client = new Client($config, $requestOptions, $logger); 50 | $this->clients[$key] = $client; 51 | 52 | return $this->clients[$key]; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Api/Providers/DashScope/Cache/DashScopeCachePointManager.php: -------------------------------------------------------------------------------- 1 | autoCacheConfig = $autoCacheConfig; 31 | } 32 | 33 | /** 34 | * 配置缓存点. 35 | * 36 | * @param ChatCompletionRequest $request 需要配置缓存点的请求对象(会直接修改此对象) 37 | */ 38 | public function configureCachePoints(ChatCompletionRequest $request): void 39 | { 40 | // 1. 估算 Token(使用 ChatCompletionRequest 内的方法) 41 | $request->calculateTokenEstimates(); 42 | 43 | // 2. 选择策略 44 | $strategy = $this->selectStrategy(); 45 | 46 | // 3. 应用策略 47 | $strategy->apply($this->autoCacheConfig, $request); 48 | } 49 | 50 | /** 51 | * 选择缓存策略. 52 | */ 53 | private function selectStrategy(): DashScopeCacheStrategyInterface 54 | { 55 | if ($this->autoCacheConfig->isAutoEnabled()) { 56 | return new AutoCacheStrategy(); 57 | } 58 | 59 | return new ManualCacheStrategy(); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Contract/Message/MessageInterface.php: -------------------------------------------------------------------------------- 1 | toToolDefinition()->toFunctionCall(); 27 | } elseif ($tool instanceof ToolDefinition) { 28 | $toolsArray[] = $tool->toFunctionCall(); 29 | } else { 30 | $toolsArray[] = $tool; 31 | } 32 | } 33 | return $toolsArray; 34 | } 35 | 36 | public static function createFromArray(array $toolArray): ?ToolDefinition 37 | { 38 | if (isset($toolArray['function'])) { 39 | $toolArray = $toolArray['function']; 40 | } 41 | $name = $toolArray['name'] ?? ''; 42 | $description = $toolArray['description'] ?? ''; 43 | $parameters = $toolArray['parameters'] ?? []; 44 | if (empty($name)) { 45 | return null; 46 | } 47 | $toolHandler = $toolArray['toolHandler'] ?? function (...$args) { 48 | // 仅定义 49 | return ''; 50 | }; 51 | return new ToolDefinition( 52 | name: $name, 53 | description: $description, 54 | parameters: ToolParameters::fromArray($parameters), 55 | toolHandler: $toolHandler 56 | ); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Api/Providers/DashScope/DashScopeConfig.php: -------------------------------------------------------------------------------- 1 | autoCacheConfig = $autoCacheConfig ?? new DashScopeAutoCacheConfig(); 29 | } 30 | 31 | public function getApiKey(): string 32 | { 33 | return $this->apiKey; 34 | } 35 | 36 | public function getBaseUrl(): string 37 | { 38 | return $this->baseUrl; 39 | } 40 | 41 | public function shouldSkipApiKeyValidation(): bool 42 | { 43 | return $this->skipApiKeyValidation; 44 | } 45 | 46 | public function getAutoCacheConfig(): DashScopeAutoCacheConfig 47 | { 48 | return $this->autoCacheConfig; 49 | } 50 | 51 | public function isAutoCache(): bool 52 | { 53 | return $this->autoCacheConfig->isAutoEnabled(); 54 | } 55 | 56 | public function toArray(): array 57 | { 58 | return [ 59 | 'api_key' => $this->apiKey, 60 | 'base_url' => $this->baseUrl, 61 | 'skip_api_key_validation' => $this->skipApiKeyValidation, 62 | ]; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Model/QianFanModel.php: -------------------------------------------------------------------------------- 1 | checkEmbeddingSupport(); 28 | 29 | if (is_string($input)) { 30 | $input = [$input]; 31 | } 32 | 33 | $client = $this->getClient(); 34 | $embeddingRequest = new EmbeddingRequest( 35 | input: $input, 36 | model: $this->model 37 | ); 38 | $embeddingRequest->setBusinessParams($businessParams); 39 | $embeddingRequest->setIncludeBusinessParams($this->includeBusinessParams); 40 | 41 | return $client->embeddings($embeddingRequest); 42 | } 43 | 44 | protected function getClient(): ClientInterface 45 | { 46 | // 处理API基础URL,确保包含正确的版本路径 47 | $config = $this->config; 48 | $this->processApiBaseUrl($config); 49 | 50 | // 使用ClientFactory创建OpenAI客户端 51 | return ClientFactory::createOpenAIClient( 52 | $config, 53 | $this->getApiRequestOptions(), 54 | $this->logger 55 | ); 56 | } 57 | 58 | protected function getApiVersionPath(): string 59 | { 60 | return 'v2'; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /examples/chat_doubao.php: -------------------------------------------------------------------------------- 1 | env('DOUBAO_BASE_URL'), 37 | 'api_key' => env('DOUBAO_API_KEY'), 38 | ], 39 | new Logger(), 40 | ); 41 | 42 | $messages = [ 43 | new SystemMessage(''), 44 | new UserMessage('你是谁?'), 45 | ]; 46 | 47 | $start = microtime(true); 48 | 49 | // 使用非流式API调用 50 | $request = new ChatCompletionRequest($messages, maxTokens: 8096); 51 | $request->setThinking([ 52 | 'type' => 'disabled', 53 | ]); 54 | $response = $model->chatWithRequest($request); 55 | 56 | // 输出完整响应 57 | $message = $response->getFirstChoice()->getMessage(); 58 | if ($message instanceof AssistantMessage) { 59 | echo '' . $message->getReasoningContent() . '' . PHP_EOL; 60 | echo $message->getContent(); 61 | } 62 | 63 | echo PHP_EOL; 64 | echo '耗时' . (microtime(true) - $start) . '秒' . PHP_EOL; 65 | -------------------------------------------------------------------------------- /src/Prompt/Prompt.php: -------------------------------------------------------------------------------- 1 | $value) { 23 | $prompt = str_replace("{ {$keyword} }", $value, $prompt); 24 | $prompt = str_replace("{$keyword}", $value, $prompt); 25 | } 26 | return $prompt; 27 | } 28 | 29 | public static function input(string $input): array 30 | { 31 | $defaultSystemMessage = self::getPrompt('DefaultSystemMessage'); 32 | return [ 33 | 'system' => new SystemMessage($defaultSystemMessage), 34 | 'user' => new UserMessage($input), 35 | ]; 36 | } 37 | 38 | public static function getPrompt(string $key, array $arguments = []): string 39 | { 40 | $prompt = match ($key) { 41 | 'DefaultSystemMessage' => file_get_contents(__DIR__ . '/DefaultSystemMessage.prompt'), 42 | 'CodeInterpreter' => file_get_contents(__DIR__ . '/CodeInterpreter.prompt'), 43 | 'AfterCodeExecuted' => file_get_contents(__DIR__ . '/AfterCodeExecuted.prompt'), 44 | 'KnowledgeAutoQA' => file_get_contents(__DIR__ . '/KnowledgeAutoQA.prompt'), 45 | }; 46 | if ($arguments) { 47 | foreach ($arguments as $key => $value) { 48 | if ($value === null) { 49 | $value = ''; 50 | } 51 | $prompt = str_replace("{{{$key}}}", $value, $prompt); 52 | } 53 | } 54 | return $prompt; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/Memory/Policy/AbstractPolicy.php: -------------------------------------------------------------------------------- 1 | options = array_merge($this->getDefaultOptions(), $options); 38 | } 39 | 40 | /** 41 | * 配置策略参数. 42 | * 43 | * @param array $options 配置选项 44 | * @return self 支持链式调用 45 | */ 46 | public function configure(array $options): self 47 | { 48 | $this->options = array_merge($this->options, $options); 49 | return $this; 50 | } 51 | 52 | /** 53 | * 处理消息列表,返回经过策略处理后的消息列表. 54 | * 55 | * @param AbstractMessage[] $messages 原始消息列表 56 | * @return AbstractMessage[] 处理后的消息列表 57 | */ 58 | abstract public function process(array $messages): array; 59 | 60 | /** 61 | * 获取默认配置选项. 62 | * 63 | * @return array 默认配置选项 64 | */ 65 | protected function getDefaultOptions(): array 66 | { 67 | return []; 68 | } 69 | 70 | /** 71 | * 获取配置参数. 72 | * 73 | * @param string $key 配置键 74 | * @param mixed $default 默认值 75 | * @return mixed 配置值 76 | */ 77 | protected function getOption(string $key, mixed $default = null): mixed 78 | { 79 | return $this->options[$key] ?? $default; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Api/Providers/OpenAI/Client.php: -------------------------------------------------------------------------------- 1 | getBaseUri() . '/chat/completions'; 35 | } 36 | 37 | /** 38 | * 构建嵌入API的URL. 39 | */ 40 | protected function buildEmbeddingsUrl(): string 41 | { 42 | return $this->getBaseUri() . '/embeddings'; 43 | } 44 | 45 | /** 46 | * 构建文本补全API的URL. 47 | */ 48 | protected function buildCompletionsUrl(): string 49 | { 50 | return $this->getBaseUri() . '/completions'; 51 | } 52 | 53 | /** 54 | * 获取认证头信息. 55 | */ 56 | protected function getAuthHeaders(): array 57 | { 58 | $headers = []; 59 | /** @var OpenAIConfig $config */ 60 | $config = $this->config; 61 | 62 | if ($config->getApiKey()) { 63 | $headers['Authorization'] = 'Bearer ' . $config->getApiKey(); 64 | } 65 | 66 | if ($config->getOrganization()) { 67 | $headers['OpenAI-Organization'] = $config->getOrganization(); 68 | } 69 | 70 | return $headers; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Event/EventCallbackListener.php: -------------------------------------------------------------------------------- 1 | logger = $this->container->get(LoggerInterface::class); 34 | } 35 | 36 | public function listen(): array 37 | { 38 | return [ 39 | AfterChatCompletionsEvent::class, 40 | AfterChatCompletionsStreamEvent::class, 41 | ]; 42 | } 43 | 44 | public function process(object $event): void 45 | { 46 | if ($event instanceof AfterChatCompletionsEvent) { 47 | $this->handleCallbacks($event); 48 | } 49 | } 50 | 51 | /** 52 | * 执行事件中注册的回调函数. 53 | */ 54 | public function handleCallbacks(AfterChatCompletionsEvent $event): void 55 | { 56 | // 执行事件中注册的回调函数 57 | foreach ($event->getCallbacks() as $callback) { 58 | try { 59 | $callback($event); 60 | } catch (Throwable $e) { 61 | $this->logger->error('Event callback execution failed: ' . $e->getMessage(), [ 62 | 'exception' => $e, 63 | ]); 64 | continue; 65 | } 66 | } 67 | // 清理 68 | $event->clearCallbacks(); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /examples/mapper/vision_stream.php: -------------------------------------------------------------------------------- 1 | get(ModelMapper::class); 36 | $model = $modelMapper->getModel($modelId); 37 | 38 | $userMessage = new UserMessage(); 39 | $userMessage->addContent(UserMessageContent::text('请分析下面图片中的内容,并描述其主要元素和可能的用途。')); 40 | $userMessage->addContent(UserMessageContent::imageUrl('https://tos-tools.tos-cn-beijing.volces.com/misc/sample1.jpg')); 41 | 42 | $start = microtime(true); 43 | 44 | // Use streaming API 45 | $response = $model->chatStream([$userMessage]); 46 | 47 | // Output streaming response 48 | /** @var ChatCompletionChoice $choice */ 49 | foreach ($response->getStreamIterator() as $choice) { 50 | $message = $choice->getMessage(); 51 | if ($message instanceof AssistantMessage) { 52 | echo $message->getReasoningContent() ?? $message->getContent(); 53 | } 54 | } 55 | 56 | echo PHP_EOL; 57 | echo '耗时' . (microtime(true) - $start) . '秒' . PHP_EOL; 58 | -------------------------------------------------------------------------------- /examples/stream.php: -------------------------------------------------------------------------------- 1 | env('DOUBAO_API_KEY'), 37 | 'base_url' => env('DOUBAO_BASE_URL'), 38 | ], 39 | new Logger(), 40 | ); 41 | 42 | $model->setApiRequestOptions(new ApiOptions([ 43 | // HTTP 处理器配置 - 支持环境变量 ODIN_HTTP_HANDLER 44 | 'http_handler' => env('ODIN_HTTP_HANDLER', 'auto'), 45 | ])); 46 | 47 | $messages = [ 48 | new SystemMessage(''), 49 | new UserMessage('请解释量子纠缠的原理,并举一个实际应用的例子'), 50 | ]; 51 | $response = $model->chatStream($messages); 52 | 53 | $start = microtime(true); 54 | /** @var ChatCompletionChoice $choice */ 55 | foreach ($response->getStreamIterator() as $choice) { 56 | $message = $choice->getMessage(); 57 | if ($message instanceof AssistantMessage) { 58 | echo $message->getReasoningContent() ?? $message->getContent(); 59 | } 60 | } 61 | echo PHP_EOL; 62 | echo '耗时' . (microtime(true) - $start) . '秒' . PHP_EOL; 63 | -------------------------------------------------------------------------------- /src/Tool/Definition/Schema/JsonSchemaValidator.php: -------------------------------------------------------------------------------- 1 | validator = new Validator(); 42 | } 43 | 44 | /** 45 | * 验证数据是否符合指定的 Schema. 46 | * 47 | * @param array $data 要验证的数据 48 | * @param array $schema Schema定义 49 | * @param int $checkMode 验证模式标志,可使用Constraint::CHECK_MODE_*常量 50 | * @return bool 验证是否通过 51 | */ 52 | public function validate(array $data, array $schema, int $checkMode = Constraint::CHECK_MODE_NORMAL): bool 53 | { 54 | $this->errors = []; 55 | $this->validator->reset(); 56 | 57 | // 将数组转换为对象,因为jsonrainbow/json-schema需要对象格式的schema和数据 58 | $schemaObject = json_decode(json_encode($schema)); 59 | $dataObject = json_decode(json_encode($data)); 60 | 61 | $this->validator->validate($dataObject, $schemaObject, $checkMode); 62 | $this->errors = $this->validator->getErrors(); 63 | 64 | return $this->validator->isValid(); 65 | } 66 | 67 | /** 68 | * 获取验证错误信息. 69 | */ 70 | public function getErrors(): array 71 | { 72 | return $this->errors; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/Model/DashScopeModel.php: -------------------------------------------------------------------------------- 1 | config; 31 | $this->processApiBaseUrl($config); 32 | 33 | $dashScope = new DashScope(); 34 | 35 | // 创建自动缓存配置 36 | $autoCacheConfig = $this->createAutoCacheConfig($config); 37 | 38 | $configObj = new DashScopeConfig( 39 | apiKey: $config['api_key'] ?? '', 40 | baseUrl: $config['base_url'] ?? 'https://dashscope.aliyuncs.com', 41 | skipApiKeyValidation: $config['skip_api_key_validation'] ?? false, 42 | autoCacheConfig: $autoCacheConfig 43 | ); 44 | 45 | return $dashScope->getClient($configObj, $this->getApiRequestOptions(), $this->logger); 46 | } 47 | 48 | /** 49 | * 创建自动缓存配置. 50 | */ 51 | private function createAutoCacheConfig(array $config): DashScopeAutoCacheConfig 52 | { 53 | $cacheConfig = $config['auto_cache_config'] ?? []; 54 | 55 | return new DashScopeAutoCacheConfig( 56 | minCacheTokens: $cacheConfig['min_cache_tokens'] ?? 1024, 57 | supportedModels: $cacheConfig['supported_models'] ?? ['qwen3-coder-plus'], 58 | autoEnabled: $cacheConfig['auto_enabled'] ?? false 59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Exception/LLMException/Model/LLMContextLengthException.php: -------------------------------------------------------------------------------- 1 | currentLength = $currentLength; 51 | $this->maxLength = $maxLength; 52 | 53 | if ($currentLength !== null && $maxLength !== null) { 54 | $message = sprintf('%s, current length: %d, max limit: %d', $message, $currentLength, $maxLength); 55 | } 56 | 57 | parent::__construct($message, self::ERROR_CODE, $previous, 0, $model, $statusCode); 58 | } 59 | 60 | /** 61 | * 获取当前上下文长度. 62 | */ 63 | public function getCurrentLength(): ?int 64 | { 65 | return $this->currentLength; 66 | } 67 | 68 | /** 69 | * 获取最大上下文长度. 70 | */ 71 | public function getMaxLength(): ?int 72 | { 73 | return $this->maxLength; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /examples/chat.php: -------------------------------------------------------------------------------- 1 | env('AZURE_OPENAI_4O_API_KEY'), 37 | 'api_base' => env('AZURE_OPENAI_4O_API_BASE'), 38 | 'api_version' => env('AZURE_OPENAI_4O_API_VERSION'), 39 | 'deployment_name' => env('AZURE_OPENAI_4O_DEPLOYMENT_NAME'), 40 | ], 41 | new Logger(), 42 | ); 43 | 44 | $model->setApiRequestOptions(new ApiOptions([ 45 | // HTTP 处理器配置 - 支持环境变量 ODIN_HTTP_HANDLER 46 | 'http_handler' => env('ODIN_HTTP_HANDLER', 'auto'), 47 | ])); 48 | 49 | $messages = [ 50 | new SystemMessage(''), 51 | new UserMessage('请解释量子纠缠的原理,并举一个实际应用的例子'), 52 | ]; 53 | 54 | $start = microtime(true); 55 | 56 | // 使用非流式API调用 57 | $response = $model->chat($messages); 58 | 59 | // 输出完整响应 60 | $message = $response->getFirstChoice()->getMessage(); 61 | if ($message instanceof AssistantMessage) { 62 | echo $message->getReasoningContent() ?? $message->getContent(); 63 | } 64 | 65 | echo PHP_EOL; 66 | echo '耗时' . (microtime(true) - $start) . '秒' . PHP_EOL; 67 | -------------------------------------------------------------------------------- /src/Api/Providers/AzureOpenAI/AzureOpenAI.php: -------------------------------------------------------------------------------- 1 | getApiKey())) { 32 | throw new LLMInvalidApiKeyException('API密钥不能为空', null, 'AzureOpenAI'); 33 | } 34 | if (empty($config->getBaseUrl())) { 35 | throw new LLMInvalidEndpointException('基础URL不能为空', null, $config->getBaseUrl()); 36 | } 37 | if (empty($config->getApiVersion())) { 38 | throw new LLMConfigurationException('API版本不能为空'); 39 | } 40 | if (empty($config->getDeploymentName())) { 41 | throw new LLMConfigurationException('部署名称不能为空'); 42 | } 43 | 44 | $requestOptions = $requestOptions ?? new ApiOptions(); 45 | 46 | $key = md5(json_encode($config->toArray()) . json_encode($requestOptions->toArray())); 47 | if (($this->clients[$key] ?? null) instanceof Client) { 48 | return $this->clients[$key]; 49 | } 50 | 51 | $client = new Client($config, $requestOptions, $logger); 52 | $this->clients[$key] = $client; 53 | return $client; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Api/Providers/AwsBedrock/MergedToolMessage.php: -------------------------------------------------------------------------------- 1 | 27 | */ 28 | private array $toolMessages; 29 | 30 | /** 31 | * @param array $toolMessages Array of ToolMessage instances 32 | */ 33 | public function __construct(array $toolMessages) 34 | { 35 | $this->toolMessages = $toolMessages; 36 | // Use the first tool message's data as base 37 | $firstMessage = $toolMessages[0]; 38 | parent::__construct( 39 | $firstMessage->getContent(), 40 | $firstMessage->getToolCallId(), 41 | $firstMessage->getName(), 42 | $firstMessage->getArguments() 43 | ); 44 | 45 | // Check all tool messages for cache points 46 | foreach ($toolMessages as $toolMessage) { 47 | if ($toolMessage->getCachePoint()) { 48 | $this->setCachePoint($toolMessage->getCachePoint()); 49 | break; // Found cache point, no need to continue 50 | } 51 | } 52 | } 53 | 54 | /** 55 | * Get all tool messages. 56 | * 57 | * @return array 58 | */ 59 | public function getToolMessages(): array 60 | { 61 | return $this->toolMessages; 62 | } 63 | 64 | /** 65 | * Check if this is a merged tool message. 66 | */ 67 | public function isMerged(): bool 68 | { 69 | return true; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /README-CN.md: -------------------------------------------------------------------------------- 1 | [English](README.md) | 中文 2 | 3 | # Odin 4 | 5 | Odin 是一个基于 PHP 的 LLM 应用开发框架,其命名灵感来自于北欧神话中的主神 Odin(奥丁)和他的两只乌鸦 Huginn 和 Muninn,Huginn 和 Muninn 分别代表的 **思想** 和 **记忆**,它们两个每天早上一破晓就飞到人间,到了晚上再将所见所闻带回给 Odin。 6 | 此项目旨在帮助开发人员利用 LLM 技术创建更加智能和灵活的应用程序,通过提供一系列强大而易用的功能,为 LLM 技术落地提供了更多的可能性。 7 | 8 | ## 核心特性 9 | 10 | - **多模型支持**:支持 OpenAI、Azure OpenAI、AWS Bedrock、Doubao、ChatGLM 等多种大语言模型 11 | - **统一接口**:提供一致的 API 接口,简化与不同 LLM 提供商的集成 12 | - **工具调用**:支持 Function Calling,允许模型调用自定义工具和函数 13 | - **MCP 集成**:基于 [dtyq/php-mcp](https://github.com/dtyq/php-mcp) 实现 Model Context Protocol 支持,轻松接入外部工具和服务 14 | - **记忆管理**:提供灵活的记忆管理系统,支持会话上下文保持 15 | - **向量存储**:集成 Qdrant 向量数据库,支持知识检索和语义搜索 16 | - **Agent 开发**:内置 Agent 框架,支持智能代理开发 17 | - **高性能**:优化的实现,支持流式响应和高效处理 18 | 19 | ## 系统要求 20 | 21 | - PHP >= 8.0 22 | - PHP 扩展:bcmath、curl、mbstring 23 | - Composer >= 2.0 24 | - Hyperf 框架 (2.2.x, 3.0.x 或 3.1.x) 25 | 26 | ## 安装 27 | 28 | ```bash 29 | composer require hyperf/odin 30 | ``` 31 | 32 | ## 快速开始 33 | 34 | 1. 安装完成后,发布配置文件: 35 | 36 | ```bash 37 | php bin/hyperf.php vendor:publish hyperf/odin 38 | ``` 39 | 40 | 2. 在 `.env` 文件中配置你的 API 密钥: 41 | 42 | ``` 43 | OPENAI_API_KEY=your_openai_api_key 44 | ``` 45 | 46 | 3. 在 `config/autoload/odin.php` 中设置默认模型: 47 | 48 | ```php 49 | return [ 50 | 'llm' => [ 51 | 'default' => 'gpt-4o', // 设置你的默认模型 52 | // ... 其他配置 53 | ], 54 | ]; 55 | ``` 56 | 57 | ## 文档 58 | 59 | 详细的文档可在 `doc/user-guide-cn` 目录中找到: 60 | - [安装和配置](doc/user-guide-cn/01-installation.md) 61 | - [核心概念](doc/user-guide-cn/02-core-concepts.md) 62 | - [API 参考](doc/user-guide-cn/03-api-reference.md) 63 | - [模型提供商](doc/user-guide-cn/04-model-providers.md) 64 | - [工具开发](doc/user-guide-cn/05-tool-development.md) 65 | - [记忆管理](doc/user-guide-cn/06-memory-management.md) 66 | - [Agent 开发](doc/user-guide-cn/07-agent-development.md) 67 | - [示例项目](doc/user-guide-cn/09-examples.md) 68 | - [MCP 集成](doc/user-guide-cn/11-mcp-integration.md) 69 | - [常见问题解答](doc/user-guide-cn/10-faq.md) 70 | 71 | ## License 72 | 73 | Odin is open-sourced software licensed under the [MIT license](https://github.com/hyperf/odin/blob/master/LICENSE). 74 | -------------------------------------------------------------------------------- /src/Memory/Policy/TokenLimitPolicy.php: -------------------------------------------------------------------------------- 1 | getOption('max_tokens', 4000); 33 | $tokenRatio = $this->getOption('token_ratio', 3.5); // 约 3.5 个字符一个 token 34 | 35 | // 如果没有消息,直接返回 36 | if (empty($messages)) { 37 | return []; 38 | } 39 | 40 | // 计算每条消息的 token 数量,从最新的消息开始保留 41 | $result = []; 42 | $totalTokens = 0; 43 | 44 | // 从最新的消息开始处理 45 | $reversedMessages = array_reverse($messages); 46 | 47 | foreach ($reversedMessages as $message) { 48 | $content = $message->getContent(); 49 | $tokenCount = (int) ceil(mb_strlen($content) / $tokenRatio); 50 | 51 | // 如果添加这条消息会超出限制,则停止添加 52 | if ($totalTokens + $tokenCount > $maxTokens && ! empty($result)) { 53 | break; 54 | } 55 | 56 | // 添加消息并累加 token 数量 57 | $totalTokens += $tokenCount; 58 | array_unshift($result, $message); // 恢复原始顺序 59 | } 60 | 61 | return $result; 62 | } 63 | 64 | /** 65 | * 获取默认配置选项. 66 | * 67 | * @return array 默认配置选项 68 | */ 69 | protected function getDefaultOptions(): array 70 | { 71 | return [ 72 | 'max_tokens' => 4000, // 默认最大 token 数量,适用于大部分大语言模型 73 | 'token_ratio' => 3.5, // 字符与 token 的大致换算比例 74 | ]; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Contract/Memory/MemoryInterface.php: -------------------------------------------------------------------------------- 1 | apiKey = $apiKey; 36 | $this->baseUrl = $baseUrl; 37 | $this->apiVersion = $apiVersion; 38 | $this->deploymentName = $deploymentName; 39 | } 40 | 41 | public function getApiKey(): string 42 | { 43 | return $this->apiKey; 44 | } 45 | 46 | public function getBaseUrl(): string 47 | { 48 | return $this->baseUrl; 49 | } 50 | 51 | public function getApiVersion(): string 52 | { 53 | return $this->apiVersion; 54 | } 55 | 56 | public function getDeploymentName(): string 57 | { 58 | return $this->deploymentName; 59 | } 60 | 61 | /** 62 | * 从配置数组创建配置对象 63 | */ 64 | public static function fromArray(array $config): self 65 | { 66 | return new self( 67 | $config['api_key'] ?? '', 68 | $config['api_base'] ?? '', 69 | $config['api_version'] ?? '2023-05-15', 70 | $config['deployment_name'] ?? '', 71 | ); 72 | } 73 | 74 | public function toArray(): array 75 | { 76 | return [ 77 | 'api_key' => $this->apiKey, 78 | 'api_base' => $this->baseUrl, 79 | 'api_version' => $this->apiVersion, 80 | 'deployment_name' => $this->deploymentName, 81 | ]; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Logger.php: -------------------------------------------------------------------------------- 1 | log('EMERGENCY', $message, $context); 23 | } 24 | 25 | public function alert(string|Stringable $message, array $context = []): void 26 | { 27 | $this->log('ALERT', $message, $context); 28 | } 29 | 30 | public function critical(string|Stringable $message, array $context = []): void 31 | { 32 | $this->log('CRITICAL', $message, $context); 33 | } 34 | 35 | public function error(string|Stringable $message, array $context = []): void 36 | { 37 | $this->log('ERROR', $message, $context); 38 | } 39 | 40 | public function warning(string|Stringable $message, array $context = []): void 41 | { 42 | $this->log('WARNING', $message, $context); 43 | } 44 | 45 | public function notice(string|Stringable $message, array $context = []): void 46 | { 47 | $this->log('NOTICE', $message, $context); 48 | } 49 | 50 | public function info(string|Stringable $message, array $context = []): void 51 | { 52 | $this->log('INFO', $message, $context); 53 | } 54 | 55 | public function debug(string|Stringable $message, array $context = []): void 56 | { 57 | $this->log('DEBUG', $message, $context); 58 | } 59 | 60 | public function log($level, string|Stringable $message, array $context = []): void 61 | { 62 | $message = (string) $message; 63 | $datetime = date('Y-m-d H:i:s'); 64 | $message = sprintf('[%s] %s %s', $level, $datetime, $message); 65 | if ($context) { 66 | $message .= sprintf(' %s', json_encode($context, JSON_UNESCAPED_UNICODE)); 67 | } 68 | echo $message . PHP_EOL; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/Prompt/OpenAIToolsAgentPrompt.php: -------------------------------------------------------------------------------- 1 | systemPrompt = $systemPrompt; 39 | } 40 | if (! is_null($userPrompt)) { 41 | $this->userPrompt = $userPrompt; 42 | } 43 | if (! is_null($placeholders)) { 44 | $this->placeholders = $placeholders; 45 | } 46 | $this->systemPrompt .= "\n" . $this->placeholders; 47 | } 48 | 49 | public function toArray(): array 50 | { 51 | return [ 52 | 'system' => new SystemMessage($this->systemPrompt), 53 | 'user' => new UserMessage($this->userPrompt), 54 | ]; 55 | } 56 | 57 | public function getSystemPrompt(string $agentScratchpad = ''): SystemMessage 58 | { 59 | return new SystemMessage(str_replace('{agent_scratchpad}', $agentScratchpad, $this->systemPrompt)); 60 | } 61 | 62 | public function getUserPrompt(string $input): UserMessage 63 | { 64 | return new UserMessage(str_replace('{input}', $input, $this->userPrompt)); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /examples/mapper/vision_base64.php: -------------------------------------------------------------------------------- 1 | get(ModelMapper::class); 35 | $model = $modelMapper->getModel($modelId); 36 | 37 | // Convert image URL to base64 format 38 | $imageUrl = 'https://tos-tools.tos-cn-beijing.volces.com/misc/sample1.jpg'; 39 | $imageData = file_get_contents($imageUrl); 40 | $base64Image = base64_encode($imageData); 41 | $imageType = 'image/jpeg'; // Default to jpeg, or detect from URL/headers if needed 42 | $dataUrl = "data:{$imageType};base64,{$base64Image}"; 43 | 44 | echo '已将图像转换为 base64 格式' . PHP_EOL; 45 | 46 | $userMessage = new UserMessage(); 47 | $userMessage->addContent(UserMessageContent::text('请分析下面图片中的内容,并描述其主要元素和可能的用途。')); 48 | $userMessage->addContent(UserMessageContent::imageUrl($dataUrl)); 49 | 50 | $start = microtime(true); 51 | 52 | // Use non-streaming API 53 | $response = $model->chat([$userMessage]); 54 | 55 | // Output complete response 56 | $message = $response->getFirstChoice()->getMessage(); 57 | if ($message instanceof AssistantMessage) { 58 | echo $message->getReasoningContent() ?? $message->getContent(); 59 | } 60 | 61 | echo PHP_EOL; 62 | echo '耗时' . (microtime(true) - $start) . '秒' . PHP_EOL; 63 | -------------------------------------------------------------------------------- /examples/chat_with_http_mcp.php: -------------------------------------------------------------------------------- 1 | env('AZURE_OPENAI_4O_API_KEY'), 47 | 'api_base' => env('AZURE_OPENAI_4O_API_BASE'), 48 | 'api_version' => env('AZURE_OPENAI_4O_API_VERSION'), 49 | 'deployment_name' => env('AZURE_OPENAI_4O_DEPLOYMENT_NAME'), 50 | ], 51 | new Logger(), 52 | ); 53 | $model->getModelOptions()->setFunctionCall(true); 54 | $model->registerMcpServerManager($mcpServerManager); 55 | 56 | $messages = [ 57 | new SystemMessage(''), 58 | new UserMessage('使用高得地图 MCP 查询 深圳 20250901 的天气情况'), 59 | ]; 60 | 61 | $start = microtime(true); 62 | 63 | // 使用非流式API调用 64 | $request = new ChatCompletionRequest($messages); 65 | $response = $model->chatWithRequest($request); 66 | 67 | // 输出完整响应 68 | $message = $response->getFirstChoice()->getMessage(); 69 | var_dump($message); 70 | 71 | echo PHP_EOL; 72 | echo '耗时' . (microtime(true) - $start) . '秒' . PHP_EOL; 73 | -------------------------------------------------------------------------------- /src/Api/Providers/DashScope/Cache/Strategy/ManualCacheStrategy.php: -------------------------------------------------------------------------------- 1 | getMessages(); 27 | $validCachePointIndex = null; 28 | 29 | // 第一轮:找到最后一个满足条件的缓存点 30 | foreach ($messages as $index => $message) { 31 | $cachePoint = $message->getCachePoint(); 32 | if ($cachePoint !== null && $cachePoint->getType() === 'ephemeral') { 33 | $isValid = true; 34 | 35 | // 检查模型支持 36 | if (! $config->isModelSupported($request->getModel())) { 37 | $isValid = false; 38 | } 39 | 40 | // 检查 token 数量 41 | $messageTokens = $message->getTokenEstimate() ?? 0; 42 | if ($messageTokens < $config->getMinCacheTokens()) { 43 | $isValid = false; 44 | } 45 | 46 | // 如果当前缓存点有效,记录其位置 47 | if ($isValid) { 48 | $validCachePointIndex = $index; 49 | } 50 | } 51 | } 52 | 53 | // 第二轮:清除所有缓存点,只保留最后一个有效的 54 | foreach ($messages as $index => $message) { 55 | $cachePoint = $message->getCachePoint(); 56 | if ($cachePoint !== null && $cachePoint->getType() === 'ephemeral') { 57 | // 只保留最后一个有效的缓存点,其他都移除 58 | if ($index !== $validCachePointIndex) { 59 | $message->setCachePoint(null); 60 | } 61 | } 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Api/Providers/OpenAI/OpenAIConfig.php: -------------------------------------------------------------------------------- 1 | apiKey = $apiKey; 37 | $this->organization = $organization; 38 | $this->baseUrl = $baseUrl; 39 | $this->skipApiKeyValidation = $skipApiKeyValidation; 40 | } 41 | 42 | public function getApiKey(): string 43 | { 44 | return $this->apiKey; 45 | } 46 | 47 | public function getBaseUrl(): string 48 | { 49 | return $this->baseUrl; 50 | } 51 | 52 | public function getOrganization(): string 53 | { 54 | return $this->organization; 55 | } 56 | 57 | public function shouldSkipApiKeyValidation(): bool 58 | { 59 | return $this->skipApiKeyValidation; 60 | } 61 | 62 | public static function fromArray(array $config): self 63 | { 64 | return new self( 65 | $config['api_key'] ?? '', 66 | $config['organization'] ?? '', 67 | $config['base_url'] ?? 'https://api.openai.com', 68 | $config['skip_api_key_validation'] ?? false, 69 | ); 70 | } 71 | 72 | public function toArray(): array 73 | { 74 | return [ 75 | 'api_key' => $this->apiKey, 76 | 'organization' => $this->organization, 77 | 'base_url' => $this->baseUrl, 78 | 'skip_api_key_validation' => $this->skipApiKeyValidation, 79 | ]; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Api/Response/ListResponse.php: -------------------------------------------------------------------------------- 1 | data; 26 | } 27 | 28 | public function setData(array $data): self 29 | { 30 | $parsedData = []; 31 | foreach ($data as $item) { 32 | if (isset($item['object'])) { 33 | switch ($item['object']) { 34 | case 'model': 35 | $parsedData[] = Model::fromArray($item); 36 | break; 37 | case 'embedding': 38 | $parsedData[] = Embedding::fromArray($item); 39 | break; 40 | } 41 | } 42 | } 43 | $this->data = $parsedData; 44 | return $this; 45 | } 46 | 47 | public function getModel(): ?string 48 | { 49 | return $this->model; 50 | } 51 | 52 | public function setModel(?string $model): self 53 | { 54 | $this->model = $model; 55 | return $this; 56 | } 57 | 58 | public function getUsage(): ?Usage 59 | { 60 | return $this->usage; 61 | } 62 | 63 | public function setUsage(?Usage $usage): self 64 | { 65 | $this->usage = $usage; 66 | return $this; 67 | } 68 | 69 | protected function parseContent(): self 70 | { 71 | $content = json_decode($this->content, true); 72 | if (isset($content['data'])) { 73 | $this->setData($content['data']); 74 | } 75 | if (isset($content['model'])) { 76 | $this->setModel($content['model']); 77 | } 78 | if (isset($content['usage'])) { 79 | $this->setUsage(Usage::fromArray($content['usage'])); 80 | } 81 | return $this; 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/Api/Providers/AzureOpenAI/Client.php: -------------------------------------------------------------------------------- 1 | azureConfig = $config; 29 | parent::__construct($config, $requestOptions, $logger); 30 | } 31 | 32 | /** 33 | * 构建聊天补全API的URL. 34 | */ 35 | protected function buildChatCompletionsUrl(): string 36 | { 37 | return $this->buildDeploymentPath() . '/chat/completions?api-version=' . $this->azureConfig->getApiVersion(); 38 | } 39 | 40 | /** 41 | * 构建嵌入API的URL. 42 | */ 43 | protected function buildEmbeddingsUrl(): string 44 | { 45 | return $this->buildDeploymentPath() . '/embeddings?api-version=' . $this->azureConfig->getApiVersion(); 46 | } 47 | 48 | /** 49 | * 构建文本补全API的URL. 50 | */ 51 | protected function buildCompletionsUrl(): string 52 | { 53 | return $this->buildDeploymentPath() . '/completions?api-version=' . $this->azureConfig->getApiVersion(); 54 | } 55 | 56 | /** 57 | * 获取认证头信息. 58 | */ 59 | protected function getAuthHeaders(): array 60 | { 61 | $headers = []; 62 | 63 | if ($this->config->getApiKey()) { 64 | $headers['api-key'] = $this->config->getApiKey(); 65 | } 66 | 67 | return $headers; 68 | } 69 | 70 | /** 71 | * 构建部署路径. 72 | */ 73 | protected function buildDeploymentPath(): string 74 | { 75 | return $this->getBaseUri() . '/openai/deployments/' . $this->azureConfig->getDeploymentName(); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/Api/Providers/AwsBedrock/AwsBedrockConfig.php: -------------------------------------------------------------------------------- 1 | autoCacheConfig) { 37 | $this->autoCacheConfig = new AutoCacheConfig(); 38 | } 39 | } 40 | 41 | public function isAutoCache(): bool 42 | { 43 | return $this->autoCache; 44 | } 45 | 46 | public function getType(): string 47 | { 48 | return $this->type; 49 | } 50 | 51 | public function getAutoCacheConfig(): AutoCacheConfig 52 | { 53 | return $this->autoCacheConfig; 54 | } 55 | 56 | /** 57 | * AWS Bedrock 不使用 API Key,此方法是为了实现接口而提供. 58 | */ 59 | public function getApiKey(): string 60 | { 61 | return ''; 62 | } 63 | 64 | /** 65 | * AWS Bedrock 不使用 Base URL,此方法是为了实现接口而提供. 66 | */ 67 | public function getBaseUrl(): string 68 | { 69 | return ''; 70 | } 71 | 72 | public function toArray(): array 73 | { 74 | return [ 75 | 'access_key' => $this->accessKey, 76 | 'secret_key' => $this->secretKey, 77 | 'region' => $this->region, 78 | 'type' => $this->type, 79 | ]; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Factory/ModelFactory.php: -------------------------------------------------------------------------------- 1 | setModelOptions($modelOptions); 55 | $apiOptions && $model->setApiRequestOptions($apiOptions); 56 | } 57 | 58 | // 验证模型实例类型 59 | $isValidModel = $model instanceof ModelInterface || $model instanceof EmbeddingInterface; 60 | if (! $isValidModel) { 61 | throw new InvalidArgumentException( 62 | sprintf('Implementation %s does not implement ModelInterface or EmbeddingInterface.', $implementation) 63 | ); 64 | } 65 | 66 | return $model; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /examples/chat_with_stdio_mcp.php: -------------------------------------------------------------------------------- 1 | env('AZURE_OPENAI_4O_API_KEY'), 50 | 'api_base' => env('AZURE_OPENAI_4O_API_BASE'), 51 | 'api_version' => env('AZURE_OPENAI_4O_API_VERSION'), 52 | 'deployment_name' => env('AZURE_OPENAI_4O_DEPLOYMENT_NAME'), 53 | ], 54 | new Logger(), 55 | ); 56 | $model->getModelOptions()->setFunctionCall(true); 57 | $model->registerMcpServerManager($mcpServerManager); 58 | 59 | $messages = [ 60 | new SystemMessage(''), 61 | new UserMessage('echo 一个字符串:odin'), 62 | ]; 63 | 64 | $start = microtime(true); 65 | 66 | // 使用非流式API调用 67 | $request = new ChatCompletionRequest($messages); 68 | $response = $model->chatWithRequest($request); 69 | 70 | // 输出完整响应 71 | $message = $response->getFirstChoice()->getMessage(); 72 | var_dump($message); 73 | 74 | echo PHP_EOL; 75 | echo '耗时' . (microtime(true) - $start) . '秒' . PHP_EOL; 76 | -------------------------------------------------------------------------------- /src/Api/Response/Model.php: -------------------------------------------------------------------------------- 1 | id; 34 | } 35 | 36 | public function setId(string $id): self 37 | { 38 | $this->id = $id; 39 | return $this; 40 | } 41 | 42 | public function getCreated(): int 43 | { 44 | return $this->created; 45 | } 46 | 47 | public function setCreated(int $created): self 48 | { 49 | $this->created = $created; 50 | return $this; 51 | } 52 | 53 | public function getOwnedBy(): string 54 | { 55 | return $this->ownedBy; 56 | } 57 | 58 | public function setOwnedBy(string $ownedBy): self 59 | { 60 | $this->ownedBy = $ownedBy; 61 | return $this; 62 | } 63 | 64 | public function getPermission(): array 65 | { 66 | return $this->permission; 67 | } 68 | 69 | public function setPermission(array $permission): self 70 | { 71 | $this->permission = $permission; 72 | return $this; 73 | } 74 | 75 | public function getRoot(): string 76 | { 77 | return $this->root; 78 | } 79 | 80 | public function setRoot(string $root): self 81 | { 82 | $this->root = $root; 83 | return $this; 84 | } 85 | 86 | public function getParent(): ?string 87 | { 88 | return $this->parent; 89 | } 90 | 91 | public function setParent(?string $parent): self 92 | { 93 | $this->parent = $parent; 94 | return $this; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Agent/Tool/MultiToolUseParallelTool.php: -------------------------------------------------------------------------------- 1 | 22 | */ 23 | private array $allTools; 24 | 25 | public function __construct(array $allTools = []) 26 | { 27 | $this->allTools = $allTools; 28 | parent::__construct( 29 | name: 'multi_tool_use.parallel', 30 | toolHandler: [$this, 'execute'] 31 | ); 32 | } 33 | 34 | public function execute($args): array 35 | { 36 | $toolUses = $args['tool_uses'] ?? []; 37 | if (empty($toolUses)) { 38 | return []; 39 | } 40 | $results = []; 41 | $toolExecutor = new ToolExecutor(); 42 | foreach ($toolUses as $toolUse) { 43 | $recipientName = $toolUse['recipient_name'] ?? ''; 44 | // 提取 function 名 45 | $functionName = explode('.', $recipientName)[1] ?? ''; 46 | // 入参 47 | $parameters = $toolUse['parameters'] ?? []; 48 | 49 | $tool = $this->allTools[$functionName] ?? null; 50 | if (! $tool) { 51 | continue; 52 | } 53 | $toolExecutor->add(function () use ($recipientName, $tool, $parameters, &$results) { 54 | $success = true; 55 | try { 56 | $callToolResult = call_user_func($tool->getToolHandler(), $parameters); 57 | } catch (Throwable $throwable) { 58 | $success = false; 59 | $callToolResult = ['error' => $throwable->getMessage()]; 60 | } 61 | $results[] = [ 62 | 'recipient_name' => $recipientName, 63 | 'success' => $success, 64 | 'result' => $callToolResult, 65 | ]; 66 | }); 67 | } 68 | $toolExecutor->run(); 69 | return $results; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /examples/mapper/vision_stream_base64.php: -------------------------------------------------------------------------------- 1 | get(ModelMapper::class); 36 | $model = $modelMapper->getModel($modelId); 37 | 38 | // Convert image URL to base64 format 39 | $imageUrl = 'https://tos-tools.tos-cn-beijing.volces.com/misc/sample1.jpg'; 40 | $imageData = file_get_contents($imageUrl); 41 | $base64Image = base64_encode($imageData); 42 | $imageType = 'image/jpeg'; // Default to jpeg, or detect from URL/headers if needed 43 | $dataUrl = "data:{$imageType};base64,{$base64Image}"; 44 | 45 | echo '已将图像转换为 base64 格式' . PHP_EOL; 46 | 47 | $userMessage = new UserMessage(); 48 | $userMessage->addContent(UserMessageContent::text('请分析下面图片中的内容,并描述其主要元素和可能的用途。')); 49 | $userMessage->addContent(UserMessageContent::imageUrl($dataUrl)); 50 | 51 | $start = microtime(true); 52 | 53 | // Use streaming API 54 | $response = $model->chatStream([$userMessage]); 55 | 56 | // Output streaming response 57 | /** @var ChatCompletionChoice $choice */ 58 | foreach ($response->getStreamIterator() as $choice) { 59 | $message = $choice->getMessage(); 60 | if ($message instanceof AssistantMessage) { 61 | echo $message->getReasoningContent() ?? $message->getContent(); 62 | } 63 | } 64 | 65 | echo PHP_EOL; 66 | echo '耗时' . (microtime(true) - $start) . '秒' . PHP_EOL; 67 | -------------------------------------------------------------------------------- /src/Api/Providers/Gemini/Cache/Strategy/GeminiMessageCacheManager.php: -------------------------------------------------------------------------------- 1 | 33 | */ 34 | private array $cachePointMessages; 35 | 36 | public function __construct(array $cachePointMessages) 37 | { 38 | ksort($cachePointMessages); 39 | $this->cachePointMessages = $cachePointMessages; 40 | } 41 | 42 | public function getCacheKey(string $model): string 43 | { 44 | return 'gemini_cache:' . md5($model . $this->getToolsHash() . $this->getSystemMessageHash() . $this->getFirstUserMessageHash()); 45 | } 46 | 47 | public function getToolsHash(): string 48 | { 49 | if (! isset($this->cachePointMessages[0])) { 50 | return ''; 51 | } 52 | return $this->cachePointMessages[0]->getHash() ?? ''; 53 | } 54 | 55 | public function getSystemMessageHash(): string 56 | { 57 | if (! isset($this->cachePointMessages[1])) { 58 | return ''; 59 | } 60 | return $this->cachePointMessages[1]->getHash() ?? ''; 61 | } 62 | 63 | /** 64 | * 获取第一个 user message 的 hash. 65 | */ 66 | public function getFirstUserMessageHash(): string 67 | { 68 | if (! isset($this->cachePointMessages[2])) { 69 | return ''; 70 | } 71 | return $this->cachePointMessages[2]->getHash() ?? ''; 72 | } 73 | 74 | public function getCachePointMessages(): array 75 | { 76 | return $this->cachePointMessages; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/Api/Providers/AwsBedrock/AwsBedrock.php: -------------------------------------------------------------------------------- 1 | accessKey) || empty($config->secretKey)) { 32 | throw new LLMInvalidApiKeyException('AWS访问密钥和密钥不能为空', null, 'AWS Bedrock'); 33 | } 34 | 35 | // 验证区域设置 36 | if (empty($config->region)) { 37 | throw new LLMInvalidEndpointException('AWS区域不能为空', null, $config->region); 38 | } 39 | 40 | $requestOptions = $requestOptions ?? new ApiOptions(); 41 | 42 | $key = md5(json_encode($config->toArray()) . json_encode($requestOptions->toArray())); 43 | if ($this->clients[$key] ?? null) { 44 | return $this->clients[$key]; 45 | } 46 | 47 | if ($config->getType() === AwsType::CONVERSE_CUSTOM) { 48 | // Use custom Converse client without AWS SDK (manual Guzzle + SigV4) 49 | $client = new ConverseCustomClient($config, $requestOptions, $logger); 50 | } elseif ($config->getType() === AwsType::CONVERSE) { 51 | // Use Converse API with AWS SDK 52 | $client = new ConverseClient($config, $requestOptions, $logger); 53 | } else { 54 | // Use InvokeModel API with AWS SDK (default) 55 | $client = new Client($config, $requestOptions, $logger); 56 | } 57 | 58 | $this->clients[$key] = $client; 59 | return $this->clients[$key]; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/Message/UserMessageContent.php: -------------------------------------------------------------------------------- 1 | type = $type; 33 | } 34 | 35 | public static function text(string $text): self 36 | { 37 | return (new self(self::TEXT))->setText($text); 38 | } 39 | 40 | public static function imageUrl(string $url): self 41 | { 42 | return (new self(self::IMAGE_URL))->setImageUrl($url); 43 | } 44 | 45 | public function getType(): string 46 | { 47 | return $this->type; 48 | } 49 | 50 | public function getText(): string 51 | { 52 | return $this->text; 53 | } 54 | 55 | public function setText(string $text): self 56 | { 57 | $this->text = trim($text); 58 | return $this; 59 | } 60 | 61 | public function getImageUrl(): string 62 | { 63 | return $this->imageUrl; 64 | } 65 | 66 | public function setImageUrl(string $imageUrl): self 67 | { 68 | $this->imageUrl = trim($imageUrl); 69 | return $this; 70 | } 71 | 72 | public function isValid(): bool 73 | { 74 | return match ($this->type) { 75 | self::TEXT => $this->text !== '', 76 | self::IMAGE_URL => $this->imageUrl !== '', 77 | default => false, 78 | }; 79 | } 80 | 81 | public function toArray(): array 82 | { 83 | return match ($this->type) { 84 | self::TEXT => [ 85 | 'type' => 'text', 86 | 'text' => $this->text, 87 | ], 88 | self::IMAGE_URL => [ 89 | 'type' => 'image_url', 90 | 'image_url' => [ 91 | 'url' => $this->imageUrl, 92 | ], 93 | ], 94 | default => [], 95 | }; 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/Api/Response/AbstractResponse.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 37 | $this->setOriginResponse($response); 38 | } 39 | 40 | public function isSuccess(): bool 41 | { 42 | return $this->success; 43 | } 44 | 45 | public function getContent(): ?string 46 | { 47 | return $this->content; 48 | } 49 | 50 | public function setContent($content): self 51 | { 52 | $this->content = $content; 53 | $this->parseContent(); 54 | return $this; 55 | } 56 | 57 | public function getOriginResponse(): PsrResponseInterface 58 | { 59 | return $this->originResponse; 60 | } 61 | 62 | public function setOriginResponse(PsrResponseInterface $originResponse): self 63 | { 64 | $this->originResponse = $originResponse; 65 | $this->success = $originResponse->getStatusCode() === 200; 66 | $this->parseContent(); 67 | return $this; 68 | } 69 | 70 | /** 71 | * 获取使用统计 72 | */ 73 | public function getUsage(): ?Usage 74 | { 75 | return $this->usage; 76 | } 77 | 78 | /** 79 | * 设置使用统计 80 | */ 81 | public function setUsage(?Usage $usage): self 82 | { 83 | $this->usage = $usage; 84 | return $this; 85 | } 86 | 87 | public function removeBigObject(): void 88 | { 89 | unset($this->originResponse, $this->logger); 90 | } 91 | 92 | abstract protected function parseContent(): self; 93 | } 94 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hyperf/odin", 3 | "type": "library", 4 | "license": "MIT", 5 | "keywords": [ 6 | "php", 7 | "hyperf" 8 | ], 9 | "description": "", 10 | "autoload": { 11 | "psr-4": { 12 | "Hyperf\\Odin\\": "src/" 13 | }, 14 | "classmap": [ 15 | "src/Api/Providers/AwsBedrock/ClassMap/" 16 | ], 17 | "exclude-from-classmap": [ 18 | "vendor/aws/aws-sdk-php/src/Api/Validator.php" 19 | ], 20 | "files": [ 21 | "src/Api/Transport/SimpleCURLClient.php" 22 | ] 23 | }, 24 | "autoload-dev": { 25 | "psr-4": { 26 | "HyperfTest\\Odin\\": "tests" 27 | } 28 | }, 29 | "require": { 30 | "php": ">=8.1", 31 | "ext-bcmath": "*", 32 | "ext-mbstring": "*", 33 | "aws/aws-sdk-php": "^3.0", 34 | "ext-curl": "*", 35 | "dtyq/php-mcp": "0.1.*", 36 | "guzzlehttp/guzzle": "^7.0|^6.0", 37 | "hyperf/cache": "~2.2.0 || 3.0.* || 3.1.*", 38 | "hyperf/config": "~2.2.0 || 3.0.* || 3.1.*", 39 | "hyperf/di": "~2.2.0 || 3.0.* || 3.1.*", 40 | "hyperf/logger": "~2.2.0 || 3.0.* || 3.1.*", 41 | "hyperf/retry": "~2.2.0 || 3.0.* || 3.1.*", 42 | "hyperf/event": "~2.2.0 || 3.0.* || 3.1.*", 43 | "hyperf/qdrant-client": "*", 44 | "justinrainbow/json-schema": "^6.3", 45 | "yethee/tiktoken": "^0.1.2" 46 | }, 47 | "require-dev": { 48 | "friendsofphp/php-cs-fixer": "^3.0", 49 | "hyperf/engine": "^2.0", 50 | "mockery/mockery": "^1.0", 51 | "phpstan/phpstan": "^1.0", 52 | "phpunit/phpunit": ">=7.0", 53 | "vlucas/phpdotenv": "^5.0" 54 | }, 55 | "suggest": { 56 | "swow/swow": "Required to create swow components.", 57 | "hyperf/engine-swow": "Required when using Swow as the event loop (^2.12).", 58 | "hyperf/engine": "Required when using Swoole as the event loop (^2.14)." 59 | }, 60 | "minimum-stability": "dev", 61 | "prefer-stable": true, 62 | "config": { 63 | "optimize-autoloader": true, 64 | "sort-packages": true 65 | }, 66 | "scripts": { 67 | "test": "phpunit -c phpunit.xml --colors=always", 68 | "analyse": "phpstan analyse --memory-limit 1024M -l 0 ./src", 69 | "cs-fix": "php-cs-fixer fix $1" 70 | }, 71 | "extra": { 72 | "hyperf": { 73 | "config": "Hyperf\\Odin\\ConfigProvider" 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /examples/exception/oversize_image_error_example.php: -------------------------------------------------------------------------------- 1 | [ 23 | 'code' => 'InvalidParameter.OversizeImage', 24 | 'message' => 'The request failed because the size of the input image (222 MB) exceeds the limit (10 MB). Request id: mock-request-id-12345', 25 | 'param' => 'image_url', 26 | 'type' => 'BadRequest', 27 | ], 28 | ]; 29 | 30 | $httpResponse = new Response(400, [], json_encode($errorResponseBody)); 31 | $httpRequest = new Request('POST', 'https://api.example-llm-provider.com/v3/chat/completions'); 32 | $requestException = new RequestException('Invalid parameter: image_url', $httpRequest, $httpResponse); 33 | 34 | try { 35 | $errorMappingManager = new ErrorMappingManager(); 36 | $llmException = $errorMappingManager->mapException($requestException); 37 | 38 | if ($llmException instanceof LLMInvalidRequestException) { 39 | echo "✅ Test PASSED - Exception correctly mapped\n"; 40 | echo 'Error Message: ' . $llmException->getMessage() . "\n\n"; 41 | 42 | // Verify provider details are preserved 43 | $providerDetails = $llmException->getProviderErrorDetails(); 44 | if ($providerDetails && isset($providerDetails['code']) && $providerDetails['code'] === 'InvalidParameter.OversizeImage') { 45 | echo "✅ Test PASSED - Provider error details preserved\n"; 46 | echo 'Error Code: ' . $providerDetails['code'] . "\n"; 47 | echo 'Error Type: ' . $providerDetails['type'] . "\n"; 48 | echo 'Error Param: ' . $providerDetails['param'] . "\n"; 49 | } else { 50 | echo "❌ Test FAILED - Provider error details missing or incomplete\n"; 51 | } 52 | } else { 53 | echo '❌ Test FAILED - Wrong exception type: ' . get_class($llmException) . "\n"; 54 | } 55 | } catch (Exception $e) { 56 | echo '❌ Test FAILED - Exception during processing: ' . $e->getMessage() . "\n"; 57 | } 58 | -------------------------------------------------------------------------------- /doc/user-guide-cn/00-introduction.md: -------------------------------------------------------------------------------- 1 | # 简介 2 | 3 | > 本文档介绍了 Odin 框架的基本概念、设计理念和核心价值。 4 | 5 | ## 什么是 Odin 6 | 7 | Odin 是一个基于 PHP 的 LLM 应用开发框架,其命名灵感来自于北欧神话中的主神 Odin(奥丁)和他的两只乌鸦 Huginn 和 Muninn。Huginn 和 Muninn 分别代表的**思想**和**记忆**,它们每天早上一破晓就飞到人间,到了晚上再将所见所闻带回给 Odin。 8 | 9 | 此项目旨在帮助开发人员利用 LLM 技术创建更加智能和灵活的应用程序,通过提供一系列强大而易用的功能,为 LLM 技术落地提供了更多的可能性。项目提供一系列便捷的工具和API,简化与各种LLM提供商(如OpenAI、Azure OpenAI、AWS Bedrock等)的集成过程。 10 | 11 | ## 设计理念 12 | 13 | Odin 的设计遵循以下核心理念: 14 | 15 | - **简单易用**:提供简洁直观的API,降低开发人员的学习成本 16 | - **高度灵活**:支持多种LLM提供商和向量数据库,适应不同场景需求 17 | - **可扩展性**:模块化设计,便于扩展和定制 18 | - **高性能**:优化的实现,支持流式响应和高效处理 19 | - **标准规范**:遵循PSR规范,保持代码质量和可维护性 20 | 21 | ## 框架架构 22 | 23 | Odin 框架的总体架构如下: 24 | 25 | ``` 26 | Odin 27 | ├── Api // 模型提供商API接口 28 | │ ├── Providers 29 | │ │ ├── OpenAI 30 | │ │ ├── AzureOpenAI 31 | │ │ └── AwsBedrock 32 | │ ├── Request // 请求相关 33 | │ ├── RequestOptions // 请求选项 34 | │ ├── Response // 响应处理 35 | │ └── Transport // 传输层 36 | ├── Model // 模型实现 37 | │ ├── OpenAIModel 38 | │ ├── AzureOpenAIModel 39 | │ ├── AwsBedrockModel 40 | │ ├── OllamaModel 41 | │ ├── ChatglmModel 42 | │ ├── DoubaoModel 43 | │ ├── RWKVModel 44 | │ └── ... 45 | ├── Message // 消息处理 46 | ├── Memory // 记忆管理 47 | ├── Tool // 工具调用 48 | │ └── Definition // 工具定义 49 | ├── Document // 文档处理 50 | ├── VectorStore // 向量存储 51 | │ └── Qdrant // Qdrant向量数据库支持 52 | ├── TextSplitter // 文本分割 53 | ├── Loader // 文档加载器 54 | ├── Knowledge // 知识库管理 55 | ├── Prompt // 提示词模板 56 | ├── Agent // 智能代理 57 | │ └── Tool // 代理工具 58 | ├── Wrapper // 外部服务包装器 59 | ├── Factory // 工厂类 60 | ├── Utils // 工具类 61 | └── Contract // 接口契约 62 | ``` 63 | 64 | ## 核心概念和术语 65 | 66 | - **LLM (Large Language Model)**:大型语言模型,如GPT、DeepSeek、Claude等 67 | - **Provider**:模型提供商,如OpenAI、Azure OpenAI、AWS Bedrock等 68 | - **Model**:模型实现,包括OpenAI、Azure OpenAI、AWS Bedrock、Ollama等多种模型支持 69 | - **Tool**:工具,可以被LLM调用的函数 70 | - **Memory**:记忆,用于存储和检索会话上下文 71 | - **Embedding**:嵌入,文本的向量表示 72 | - **Vector Store**:向量数据库,用于存储和检索向量,如Qdrant 73 | - **Knowledge**:知识库,用于管理和检索知识 74 | - **Prompt**:提示词,用于引导模型生成内容 75 | - **Agent**:代理,能够规划和执行任务的智能体 76 | - **RAG (Retrieval Augmented Generation)**:检索增强生成,通过检索相关信息来增强生成能力 77 | - **Wrapper**:外部服务包装器,用于简化与外部服务的集成,如Tavily搜索API 78 | 79 | ## 下一步 80 | 81 | - 查看[安装和配置](./01-installation.md)指南开始使用 Odin 82 | - 了解[核心概念](./02-core-concepts.md)深入理解框架设计 83 | - 浏览[示例项目](./11-examples.md)学习实际应用案例 84 | --------------------------------------------------------------------------------