├── .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 |
--------------------------------------------------------------------------------