PHP code example of likun-mci / php-ai

1. Go to this page and download the library: Download likun-mci/php-ai library. Choose the download type require.

2. Extract the ZIP file and open the index.php.

3. Add this code to the index.php.
    
        
<?php
require_once('vendor/autoload.php');

/* Start to develop here. Best regards https://php-download.com/ */

    

likun-mci / php-ai example snippets




use Ai\AI;



use Ai\AI;

use Ai\AI;

$ai = AI::create([
    'model'   => 'gpt-4o',
    'api_key' => 'sk-xxxxxxxxxxxxx',
]);

$response = $ai->chat('用一句话介绍人工智能');

echo $response->getContent();          // 回复文本
echo $response->tokens();              // 总消耗 tokens

$ai = AI::create(['protocol' => 'qwen',     'model' => 'qwen-plus',  'api_key' => 'sk-xxx']);  // 通义千问
$ai = AI::create(['protocol' => 'zhipu',    'model' => 'glm-4.6',    'api_key' => 'xxx']);     // 智谱 GLM
$ai = AI::create(['protocol' => 'doubao',   'model' => 'doubao-seed-1-6', 'api_key' => 'xxx']);// 豆包
$ai = AI::create(['protocol' => 'moonshot', 'model' => 'kimi-latest','api_key' => 'sk-xxx']);  // Kimi

// 模型名能识别出平台时,protocol 也可以省略
$ai = AI::create(['model' => 'qwen-plus', 'api_key' => 'sk-xxx']);

$response = $ai->chat([
    'messages' => [
        ['role' => 'system',    'content' => '你是一个有帮助的助手'],
        ['role' => 'user',      'content' => '介绍一下人工智能'],
        ['role' => 'assistant', 'content' => '人工智能是……'],
        ['role' => 'user',      'content' => '再简短一点'],
    ],
    'temperature' => 0.7,
    'max_tokens'  => 1000,
]);

// 换平台只改这两行,业务代码不动
$ai = AI::create([
    'protocol' => 'qwen',          // 见下表
    'model'    => 'qwen-plus',
    'api_key'  => 'sk-xxx',
]);
echo $ai->chat('你好')->getContent();

// 官方新模型:识别出 claude 家族,自动使用 api.anthropic.com/v1/messages
$ai = AI::create(['model' => 'claude-opus-5', 'api_key' => 'sk-ant-xxx']);

// 国产新模型:识别出 zhipu 家族,自动使用 open.bigmodel.cn/api/paas/v4/chat/completions
$ai = AI::create(['model' => 'glm-4.6', 'api_key' => 'xxx']);

// 开源模型 / 第三方接口:任意模型名 + 手选协议 + 自定义地址
$ai = AI::create([
    'model'    => 'llama3',
    'protocol' => 'openai',                    // 手选协议格式
    'base_url' => 'http://10.0.0.9:11434/v1',  // 自定义接口地址
]);

// 用 GLM-4.6 跑工具调用:协议是 Claude 的,价格与密钥是智谱的
$ai = AI::create([
    'protocol' => 'zhipu-anthropic',
    'model'    => 'glm-4.6',
    'api_key'  => $config['zhipu__api_key'],
]);

$ai = new AI();

// —— 不发请求的元信息查询(适合渲染后台下拉框) ——
$ai->listPlatforms();        // 37 个平台:['deepseek'=>'DeepSeek 深度求索','qwen'=>'阿里云百炼(通义千问)',...]
                             // 键即业务层约定的密钥前缀 {平台}__api_key
$ai->listProtocols();        // 40 个协议:['openai'=>'OpenAI 兼容(Chat Completions)','qwen'=>'阿里云百炼 / 通义千问(OpenAI 兼容)',...]
$ai->listProtocolGroups();   // 同上,但按「中国大陆 / 海外主流 / 聚合中转 / 本地部署」分组,可直接渲染 optgroup
$ai->listKnownModels('qwen');// 该平台的常用模型:['qwen3-max'=>'通义千问 3 Max','qwen-max'=>'通义千问 Max',...]
$ai->platformOfModel('qwen-max');   // 'qwen'(设置模型前即可安全调用,无法归属官方平台时返回 custom)

// —— 设置模型后的当前状态 ——
$ai->setConfig(['model' => 'glm-4.6', 'api_key' => 'xxx']);
$ai->getPlatform();     // 'zhipu'
$ai->getProtocolKey();  // 'zhipu',当前实际使用的协议
$ai->resolveEndpoint(); // 'https://open.bigmodel.cn/api/paas/v4/chat/completions',当前实际请求端点
$ai->listKnownModels(); // 不传参数时取当前协议的内置清单

// —— 实时调用平台接口拉取模型 ——
$ai->listModels();      // 端点跟随 base_url / endpoint 走,接第三方网关时列的就是网关的模型
                        // 拉取失败或平台无此接口时:若请求的是官方域名,回退到内置常用清单;
                        // 接的是第三方网关则返回 null(避免把官方清单误当成网关的能力)
$ai->listModels(true);  // 传入 true 返回完整模型数据(含 id / created / owned_by / pricing 等)
                        // 适用于 OpenRouter、硅基流动等需要展示模型价格/能力标签的场景

$platforms = $this->ai->listPlatforms();
$result    = [];

foreach ($platforms as $platform => $platformName) {
    $apiKey = (string)($this->siteConfig["{$platform}__api_key"] ?? '');
    if (empty($apiKey)) continue;               // 未配置 Key 的平台直接跳过

    try {
        $ai = new AI();
        // 该平台任一模型即可(用于确定协议),直接取库内置清单的第一个
        $known = $ai->listKnownModels(\Ai\Helpers\Protocols::platformProtocols($platform)[0] ?? '');
        $ai->setConfig([
            'model'   => (string)array_key_first($known),
            'api_key' => $apiKey,
        ]);
        $models = $ai->listModels();
        if (is_array($models) && $models) $result[$platform] = $models;
    } catch (\Exception $e) {
        // 单平台失败不影响其它平台
    }
}

$ai->setConfig([
    'model'        => 'gpt-4o',      // 模型标识(必填),内置标识或任意自定义模型名
    'api_key'      => 'sk-xxx',      // API 密钥(自建/内网接口可不填)
    'protocol'     => '',            // 手选协议格式:见「平台一览」,或自定义协议类名
    'tools'        => [],            // 工具定义(统一格式,见「Agent:工具调用循环」)
    'tool_choice'  => null,          // auto / any / none / ['type'=>'tool','name'=>'x']
    'base_url'     => '',            // 接口根地址,与协议官方路径智能拼接,见「自定义接口地址」
    'endpoint'     => '',            // 完整对话端点,原样使用,优先级高于 base_url
    'endpoint_models' => '',         // 完整模型列表端点(仅 listModels 生效)
    'platform'     => '',            // 平台名,仅供业务层标识,默认由模型名/协议决定
    'headers'      => [],            // 追加/覆盖请求头,值为 null 表示删除协议默认头
    'extra_body'   => [],            // 追加到请求体的私有参数
    'search'       => false,         // 联网搜索,true 或细化数组,见「联网搜索」
    'max_tokens'             => 1024 * 64,  // 最大输出 tokens
    'max_completion_tokens'  => 16384,      // 仅 OpenAI o1/o3 系列,覆盖 max_tokens
    'temperature'            => 0.7,        // 温度
    'organization' => 'org-xxx',     // 仅 OpenAI 企业账号
    'project_id'   => 'proj_xxx',    // 仅 OpenAI 企业账号
]);

$ai->setConfig([
    'headers'    => [
        'Authorization' => null,      // 删掉协议默认写入的 Bearer 头
        'X-Api-Token'   => 'abc123',  // 换成对方要求的鉴权头
    ],
    'extra_body' => ['enable_thinking' => false, 'safe_mode' => 1],
]);

$ai->setModel('claude-3-opus')   // 单独切换模型
   ->setTimeout(300)             // 超时(秒),长文本生成务必调大
   ->setConnectTimeout(30)       // 连接超时(秒),独立于总超时
   ->setUserAgent('MyApp/1.0')   // 自定义 User-Agent(默认不发送)
   ->setSslVerify(false)         // 禁用 SSL 校验(仅调试/内网自签)
   ->setProxy('socks5h://127.0.0.1:1080')
   ->setStream(true)
   ->setStreamCallback($fn)      // 流式分片交给回调(常驻内存框架必用,见「流式输出」章节)
   ->setAttachments([$file])
   ->chat($prompt);

if (!empty($config['PROXY_SOCKS5'])) {
    $ai->setProxy($config['PROXY_SOCKS5']);
} elseif (!empty($config['PROXY_HTTP'])) {
    $ai->setProxy($config['PROXY_HTTP']);
}

$ai = AI::create([
    'model'    => 'deepseek-chat',
    'api_key'  => 'sk-xxx',
    'base_url' => 'https://proxy.example.com',   // 或带端口 http://127.0.0.1:8080
]);
// 实际请求 => https://proxy.example.com/v1/chat/completions

$ai = AI::create([
    'model'    => 'deepseek-chat',
    'api_key'  => 'sk-xxx',
    'endpoint' => 'https://proxy.example.com/openai/deepseek/chat',  // 原样使用
]);

// 方式一(推荐):手选 protocol = openrouter,自动使用 openrouter.ai/api
$ai = AI::create([
    'model'    => 'openai/gpt-4o',              // OpenRouter 上的完整模型标识
    'protocol' => 'openrouter',
    'api_key'  => 'sk-or-v1-xxxxxxxxx',         // OpenRouter API Key
    'referer'  => 'https://myapp.com',          // 可选,来源标识(OpenRouter 后台查看)
    'title'    => 'MyApp',                      // 可选,应用名称
]);

// 方式二:用 base_url 配置(不依赖 protocol=openrouter)
$ai = AI::create([
    'model'    => 'anthropic/claude-sonnet-4-20250514',
    'base_url' => 'https://openrouter.ai/api',
    'api_key'  => 'sk-or-v1-xxxxxxxxx',
]);

$ai = AI::create([
    'protocol' => 'openrouter',
    'api_key'  => 'sk-or-v1-xxx',
]);
$models = $ai->setModel('openai/gpt-4o')->listModels(true);
// 返回含 id、pricing、context_length 等的完整模型信息

// API2D(国内 OpenAI 中转)
AI::create(['model'=>'gpt-4o', 'protocol'=>'openai', 'api_key'=>'fkxxxxx',
            'base_url'=>'https://openapi.api2d.com']);

// Cloudflare AI Gateway(官方聚合网关)
AI::create(['model'=>'@cf/meta/llama-3-8b-instruct', 'protocol'=>'openai', 'api_key'=>'xxx',
            'base_url'=>'https://gateway.ai.cloudflare.com/v1/{account_id}/{gateway_id}');

// one-api / new-api(自建聚合网关,最通用)
AI::create(['model'=>'glm-4.6', 'protocol'=>'openai', 'api_key'=>'sk-xxx',
            'base_url'=>'https://gateway.example.com']);

// 自建 Anthropic 兼容网关(Agent 工具调用需要 claude 协议)
AI::create(['model'=>'my-agent-model', 'protocol'=>'anthropic', 'api_key'=>'k',
            'base_url'=>'http://127.0.0.1:8080/gw']);
// => http://127.0.0.1:8080/gw/v1/messages

// 内网自建服务(无需 Key,用私有鉴权头)
AI::create(['model'=>'llama3', 'protocol'=>'openai',
            'base_url'=>'http://10.0.0.9:11434/v1',
            'headers' =>['Authorization'=>null, 'X-Internal-Token'=>'t']]);

// 内置协议 + 自定义地址:走自己的代理,但仍按该平台的协议与鉴权方式通信
AI::create(['model'=>'qwen-plus', 'protocol'=>'qwen', 'api_key'=>'sk-xxx',
            'base_url'=>'https://my-proxy.example.com/dashscope']);

echo $ai->resolveEndpoint();   // 返回按配置解析后的实际端点

use Ai\Cli\ClaudeCode;

$cli = ClaudeCode::create([
    'workdir' => '/var/www',   // 工作目录(AI 的文件操作都发生在这里)
]);

$response = $cli->chat('检查一下 index.php 的语法,有问题直接改');
echo $response->getContent();   // 最终文本
echo $response->getSessionId(); // 会话 ID
echo $response->getCostUsd();   // CLI 实测费用(USD)

$cli = ClaudeCode::create();            // 自动检测(含缓存)
$cli->setBinary('/usr/local/bin/claude'); // 手动指定(优先级最高)

// 缓存控制
$cli->setBinaryCacheEnabled(true);      // 是否启用文件缓存(默认 true)
$cli->setBinaryCacheTtl(86400);         // 缓存有效期秒(默认 1 天)
$cli->clearBinaryCache();               // 清除缓存,强制重新探测

$cli
    ->setFlag('allowedTools', 'Read Grep Glob')   // 收紧工具权限
    ->setFlag('model', 'claude-sonnet-4-5')        // 指定模型
    ->setFlag('max-turns', 3)                      // 限制轮数
    ->setFlag('              // 删除默认参数
    ->resetFlags();                                // 恢复默认

$cli
    // 权限与工具
    ->setPermissionMode('acceptEdits')      // acceptEdits/auto/bypassPermissions/manual/dontAsk/plan
    ->setAllowedTools(['Read', 'Bash(git *)'])  // 免提示白名单,支持细粒度写法
    ->setDisallowedTools(['Bash'])          // 硬性拒绝(工具从工具集移除)
    ->setTools(['Read', 'Grep', 'Glob'])    // 限定可用工具集,传 [] 禁用全部工具
    ->setAddDirs(['/data/shared'])          // 工作目录之外还允许访问的目录
    ->setSkipPermissions(false)             // --dangerously-skip-permissions

    // 模型与推理
    ->setModel('claude-sonnet-5')
    ->setFallbackModel(['sonnet', 'haiku']) // 主模型过载时按序降级
    ->setEffort('high')                     // low/medium/high/xhigh/max
    ->setThinkingTokens(31999)              // 思考预算(IDE 插件用的就是这个值)

    // 成本与产出
    ->setMaxBudgetUsd(0.5)                  // 超预算即终止,无人值守强烈建议设置
    ->setJsonSchema(['type' => 'object', 'properties' => [...]])  // 结构化输出

    // 提示词与扩展
    ->setSystemPrompt('你是代码审查专家')
    ->appendSystemPrompt('输出务必用中文')
    ->setAgent('reviewer')
    ->setAgents(['reviewer' => ['description' => '代码审查', 'prompt' => '...']])
    ->setMcpConfig(['mcpServers' => ['fs' => ['command' => 'npx']]])
    ->setStrictMcpConfig()

    // 会话与配置源
    ->setSettingSources(['user', 'project'])   // 传 [] 表示不加载任何设置文件
    ->setSettings('/path/settings.json')
    ->setFixedSessionId('550e8400-e29b-41d4-a716-446655440000')  // 指定新会话 ID
    ->setForkSession()                      // 续接时分叉,不污染原会话
    ->setContinueLast()                     // 续接当前目录最近一次会话
    ->setSessionPersistence(false)          // 会话不落盘

    // 输出与诊断
    ->setPartialMessages()                  // token 级增量事件
    ->setIncludeHookEvents()
    ->setForwardSubagentText()
    ->setDebug('api,hooks')                 // --debug + --debug-to-stderr,日志走 stderr 事件
    ->setBare()                             // 精简模式,跳过 hooks/LSP/CLAUDE.md 自动发现
    ->setSafeMode();                        // 禁用全部自定义配置,排查用

$cli->setJsonSchema([
    'type'       => 'object',
    'properties' => [
        'severity' => ['type' => 'string'],
        'issues'   => ['type' => 'array', 'items' => ['type' => 'string']],
    ],
    '

$cli->runStream('帮我重构这段代码', function ($event, $data) {
    switch ($event) {
        case 'start':          break;  // ['resume' => bool]
        case 'init':           break;  // 会话初始信息:cwd、session_id、可用工具、MCP 服务器
        case 'text':           break;  // 助手正文文本块(string)
        case 'thinking':       break;  // 助手思考内容(string)
        case 'tool_use':       break;  // ['id','name','input']
        case 'tool_result':    break;  // ['tool_use_id','content','is_error']
        case 'text_delta':     break;  // token 级正文增量(需 setPartialMessages())
        case 'thinking_delta': break;  // token 级思考增量(需 setPartialMessages())
        case 'rate_limit':     break;  // 限流状态
        case 'system':         break;  // 其它 system 子类型(thinking_tokens、compact_boundary…)
        case 'error':          break;  // 本轮标记 is_error
        case 'message':        break;  // 原始 stream-json 事件(所有事件都会先经过这里)
        case 'stderr':         break;  // 排查日志(开 setDebug() 后调试输出走这里)
        case 'result':         break;  // 最终汇总
        case 'done':           break;
    }
});

$res = $cli->chat('第一问');
$cli->setSessionId($res->getSessionId());   // 下一轮自动 --resume 续接

$cli->chat('第二问', ['reset' => true]);    // 强制开启新会话

$cli->setRunner(function ($cmd, $onChunk) {
    $stream = ssh2_exec($conn, $cmd);         // 在宿主机执行同一命令
    $err = ssh2_fetch_stream($stream, SSH2_STREAM_STDERR);
    while ($buf = fread($stream, 8192))       { $onChunk($buf, 'out'); }
    while ($buf = fread($err, 8192))          { $onChunk($buf, 'err'); }
    return 0;
});

$cli = ClaudeCode::create();

// —— 子命令类查询(毫秒级)——
$cli->getVersion();       // '2.1.222'(实例内缓存)
$cli->isLoggedIn();       // true
$cli->getAuthStatus();    // ['loggedIn'=>true,'authMethod'=>'claude.ai','apiProvider'=>'firstParty',
                          //  'email'=>'...','orgId'=>'...','orgName'=>'...','subscriptionType'=>'max']
$cli->doctor();           // 安装体检报告(原始文本)
$cli->runCommand(['mcp', 'list']);   // 任意子命令:['exit_code'=>0,'stdout'=>'...','stderr'=>'']

// —— 控制协议类查询(秒级,内部临时起一个 claude 进程问完即关)——
$cli->listModels();       // ['default','opus[1m]','claude-fable-5[1m]','sonnet','haiku','opus']
                          // 返回值可直接传给 setModel()
$cli->listModels(true);   // 完整条目,含 resolvedModel / displayName / description /
                          // supportsEffort / supportedEffortLevels / supportsFastMode 等
$cli->getUsage();         // 用量与限流全量数据
$cli->getRateLimits();    // 精简后的额度概览(见下)
$cli->getSettings();      // 合并 user/project/local 后实际生效的设置
$cli->getMcpServers();    // MCP 服务器状态列表
$cli->getBinaryVersion(); // ['version'=>'2.1.222','buildTime'=>'2026-08-04T01:24:05Z']

foreach ($cli->getRateLimits() as $limit) {
    printf("%-14s 已用 %5.1f%%  %s后重置\n",
        $limit['key'],                        // session / weekly_all / weekly_scoped
        $limit['percent'],                    // 已用百分比
        gmdate('H:i:s', $limit['resets_in'])  // 距重置剩余秒数
    );
}
// session        已用  16.0%  01:49:47后重置
// weekly_all     已用  17.0%  06:39:47后重置
// weekly_scoped  已用   0.0%  06:39:47后重置

$s = ClaudeCodeSession::create(['workdir' => '/var/www']);
$s->send('第一问');
$s->send('第二问');

$usage = $s->getUsage();
echo $usage['session']['total_cost_usd'];   // 两轮累计花费
echo $s->getSessionCost();
// Total cost:            $0.0067
// Total duration (API):  36s
// Total code changes:    0 lines added, 0 lines removed
// Usage by model:
//     claude-haiku-4-5:  10 input, 52 output, 14.1k cache read, 2.5k cache write ($0.0067)

use Ai\Cli\ClaudeCodeSession;

$s = ClaudeCodeSession::create([
    'workdir'      => '/var/www',
    'turn_timeout' => 300,        // 单轮等待上限(秒)
]);

// 工具权限实时决策,等价于 IDE 里弹出的"是否允许执行"
$s->onPermission(function (array $req) {
    // $req: tool_name / display_name / input / description / tool_use_id / permission_suggestions
    if ($req['tool_name'] === 'Bash')  return '本环境禁止执行 shell 命令';   // 字符串 = 拒绝并说明理由
    if ($req['tool_name'] === 'Write' && strpos($req['input']['file_path'], '/etc/') === 0) {
        return false;                                                       // false = 拒绝
    }
    return true;                                                            // true = 放行
    // 也可返回 ['behavior' => 'allow', 'updatedInput' => [...]] 放行并改写入参
});

$a = $s->send('看一下 src 目录结构');
$b = $s->send('把刚才第一个文件的注释补全');   // 同一进程,上下文常驻,无需 --resume 重放
$s->close();

$fired = false;
$res = $s->send('全量重构整个项目', function ($ev, $d) use ($s, &$fired) {
    if ($ev === 'tool_use' && !$fired) {
        $fired = true;
        $s->interrupt();          // 相当于 IDE 里的"停止"按钮,进程保活可继续下一轮
    }
});
echo $res->getSubtype();          // error_during_execution

$s->setPermissionMode('plan');    // 已启动时热切换(未启动则只改启动参数)
$s->switchModel('claude-haiku-4-5-20251001');
$s->switchThinkingTokens(31999);
$s->control(['subtype' => 'set_cwd', 'cwd' => '/srv/app']);   // 发送任意 control_request

$ai = AI::create(['model' => 'deepseek-chat', 'api_key' => 'sk-xxx']);

$response = $ai->setStream(true)->chat('写一篇关于人工智能的文章');

// 流式结束后仍可拿到完整内容与 tokens
$full = $response->getContent();
$used = $response->tokens();

$ai = new \Ai\AI();

if (!empty($siteConfig['PROXY_SOCKS5'])) $ai->setProxy($siteConfig['PROXY_SOCKS5']);

try {
    $ai->setStream(true)
       ->setConfig(['model' => $model, 'api_key' => $apiKey])
       ->setAttachments($attachments)
       ->chat($message);
} catch (\Ai\Exceptions\AIException $e) {
    // 流式已开始输出时,异常信息也只能顺着流写出去
    echo "data: " . json_encode(['type' => 'error', 'message' => $e->getMessage()]) . "\n\n";
}

$ai->setStream(true)->setStreamCallback(function($event) use ($response) {
    if ($event['type'] === 'stream_chunk' && $event['content'] !== null) {
        // $response 是 Swoole\Http\Response,write() 分块下发
        $response->write('data: ' . json_encode(['content' => $event['content']]) . "\n\n");
    }
    if ($event['type'] === 'stream_end') {
        $response->write('data: ' . json_encode($event['data']) . "\n\n");
    }
});

$full = $ai->chat($messages)->getContent();   // 返回值不受影响,仍是拼好的完整文本

$resp = $ai->setStream(true)->chat(['messages' => $msgs, 'tools' => $toolDefs]);

$resp->getStopReason();   // end_turn / max_tokens / tool_use …(可据此判断是否被截断)
$resp->getToolCalls();    // 分片已自动重组,格式与非流式完全一致

use Ai\Helpers\AIFile;

$image = AIFile::fromPath('/path/to/image.jpg');            // 本地文件,自动识别 MIME
$image = AIFile::fromPath($tmpName, $_FILES['x']['type']);  // 指定 MIME
$image = AIFile::fromUrl('https://example.com/image.jpg');  // 远程 URL

$response = $ai->setAttachments([$image])->chat('描述这张图片');

$ai = AI::create([
    'protocol' => 'qwen',
    'model'    => 'qwen-plus',
    'api_key'  => 'sk-xxx',
    'rounds'   => 5,          // 保留最近 5 轮上下文;默认 0 = 不启用
]);

echo $ai->chat('我叫小明')->getContent();
echo $ai->chat('我叫什么名字?')->getContent();   // 模型能答出"小明"

$ai->setSessionId('user-1001')->chat('我喜欢喝咖啡');
$ai->setSessionId('user-2002')->chat('我喜欢喝茶');      // 与上一个会话互不干扰

$ai->setSessionId('user-1001')->chat('我喜欢喝什么?');  // 答"咖啡"

// 请求结束时存起来
$redis->set("ai:history:{$uid}", json_encode($ai->exportHistory()));

// 下次请求恢复
$ai->importHistory(json_decode($redis->get("ai:history:{$uid}"), true) ?: []);

$messages = [];
$messages[] = ['role' => 'user', 'content' => '我叫小明'];
$resp = $ai->chat(['messages' => $messages]);

$messages[] = ['role' => 'assistant', 'content' => $resp->getContent()];
$messages[] = ['role' => 'user', 'content' => '我叫什么名字?'];
$resp = $ai->chat(['messages' => $messages]);

$ai->onBefore(function (&$payload) {
    // 请求前:可审计、可改写 payload
    log_message('debug', json_encode($payload));
    $payload['temperature'] = 0.5;
});

$ai->onAfter(function ($response) {          // onResponse() 是它的别名
    // 请求后:统计 tokens、写入用量表
    log_usage($response->getUsage());
});

use Ai\Exceptions\AIException;
use Ai\Exceptions\ConfigException;
use Ai\Exceptions\RequestException;

try {
    $response = $ai->chat($prompt);
} catch (ConfigException $e) {
    // 配置错误:模型不存在、未设置模型、协议未初始化
} catch (AIException $e) {
    // 请求失败:网络、鉴权、限流、平台报错,均在 chat() 内被包装成 AIException
    echo $e->getMessage();
    echo $e->getPlatform();      // 出错平台,如 'openai'
    echo $e->getErrorCode();     // 平台错误码
    print_r($e->getRawResponse()); // 平台原始错误响应,排查问题的关键
}

$ai->setTimeout(120);         // 默认 120 秒(推理模型单次经常跑一两分钟,60 秒会误杀)
$ai->setRetry(2);             // 默认重试 2 次;传 0 关闭
$ai->setRetry(3, 800, 30000); // 重试 3 次、退避基数 800ms、单次等待上限 30 秒

$ai->setTransport(new MyTransport());   // 实现 Ai\Contracts\TransportInterface 即可

$results = $ai->chatBatch([
    'title' => '把这句翻译成英文:你好',
    'intro' => '把这句翻译成英文:世界',
    'body'  => ['messages' => [['role' => 'user', 'content' => '翻译:再见']]],
], 5);   // 第二个参数是并发度,默认 5

foreach ($results as $key => $r) {
    if ($r->isSuccess()) {
        echo $key, ': ', $r->getContent(), "\n";
    } else {
        echo $key, ' 失败: ', $r->getError(), "\n";   // 单条失败不影响其它条
    }
}

use Ai\Helpers\Log;

Log::setLogger($monolog);                  // 任何 PSR-3 风格对象(不引入 psr/log 依赖)
Log::setLogger(function ($level, $message, array $context) {
    log_message($level, '[AI] ' . $message . ' ' . json_encode($context));
});
Log::setLogger(null);                      // 恢复默认的 error_log

$agent = (new Agent($ai))->setTools($tools)->onEvent($handler);
$agent->run([['role' => 'user', 'content' => '北京天气怎么样']]);

// 上面这段代码,$ai 换成下面任意一个都不用改:
AI::create(['protocol' => 'qwen',     'model' => 'qwen-plus',       'api_key' => '...']);
AI::create(['protocol' => 'zhipu',    'model' => 'glm-4.6',         'api_key' => '...']);
AI::create(['protocol' => 'doubao',   'model' => 'doubao-seed-1-6', 'api_key' => '...']);
AI::create(['protocol' => 'openai',   'model' => 'gpt-4o',          'api_key' => '...']);
AI::create(['protocol' => 'claude',   'model' => 'claude-opus-5',   'api_key' => '...']);

$ai->setStreamCallback(function ($event) use ($response) {
    if ($event['type'] === 'stream_chunk' && $event['content'] !== null) {
        $response->write('data: ' . json_encode(['content' => $event['content']]) . "\n\n");
    }
});

(new Agent($ai))
    ->setStream(true)          // 默认关闭,与旧版本一致
    ->setTools($tools)
    ->run([['role' => 'user', 'content' => '北京天气怎么样']]);

$resp = $ai->chat(['messages' => $messages, 'tools' => $toolDefs]);

if ($resp->hasToolCalls()) {                      // 各平台一致
    $messages[] = $resp->toAssistantMessage();    // 把模型这一轮接回上下文

    $results = [];
    foreach ($resp->getToolCalls() as $call) {    // ['id'=>.., 'name'=>.., 'input'=>[..]]
        $results[] = [
            'type'        => 'tool_result',
            'tool_use_id' => $call['id'],
            'content'     => myHandler($call['name'], $call['input']),
        ];
    }
    $messages[] = ['role' => 'user', 'content' => $results];

    $resp = $ai->chat(['messages' => $messages, 'tools' => $toolDefs]);
}

echo $resp->getContent();
echo $resp->getStopReason();   // end_turn / tool_use / max_tokens / content_filter / refusal

$tools = [
    'sql_query' => [
        'description'  => '执行只读 SQL 查询。仅支持 SELECT/SHOW/DESCRIBE/EXPLAIN,最多返回 200 行。',
        'input_schema' => [
            'type'       => 'object',
            'properties' => [
                'sql' => ['type' => 'string', 'description' => '要执行的 SQL'],
            ],
            '

$ai = new \Ai\AI();
$ai->setConfig([
    'model'      => 'deepseek-anthropic',
    'api_key'    => $apiKey,
    'max_tokens' => 1024 * 64,
]);

$emit = function ($ev) {
    // 把 Agent 过程实时推给前端
    echo "data: " . json_encode($ev, JSON_UNESCAPED_UNICODE) . "\n\n";
    flush();
};

$agent = (new \Ai\Agent\Agent($ai))
    ->setSystem($systemPrompt)
    ->setTools($tools)
    ->setMaxIter(128)      // 需要逐页分析的任务要给足迭代次数,默认 25
    ->onEvent($emit);

$agent->run([['role' => 'user', 'content' => $message]]);

$reply = $agent->lastText();   // 最终自然语言回复

use Ai\Editor\EditContext;
use Ai\Editor\EditProtocol;
use Ai\Editor\EditExecutor;
use Ai\Editor\EditAction;

// 1. 组装编辑上下文
$ctx = (new EditContext(FCPATH))
    ->setFile('templates/default/index.php')
    ->setLanguage('php')
    ->setContent($fileContent)
    ->setCursor(['line' => 42, 'column' => 8])
    ->setSelection(['start' => [...], 'end' => [...]], $selectedText)
    ->setOpenedFiles($openedFiles)
    ->setWorkspace($workspace);      // Ai\Editor\Workspace,限定可写根目录与编码规范

// 2. 生成系统提示词(内含编辑协议说明)+ 上下文 JSON
$system  = EditProtocol::systemPrompt($ctx);
$ctxJson = json_encode($ctx->toPromptJson(), JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

$response = $ai->chat([
    'system'   => $system,
    'messages' => [['role' => 'user', 'content' => $message . "\n\n[CONTEXT]\n

$ai = AI::create([
    'model'   => 'qwen-plus',
    'api_key' => 'sk-xxx',
    'search'  => true,          // 开启联网搜索
]);

echo $ai->chat('今天有哪些重要新闻?')->getContent();

$ai->setConfig(['model' => 'claude-sonnet-4-20250514', 'api_key' => 'sk-ant-xxx']);
$ai->setConfig(['model' => 'glm-4-plus',               'api_key' => 'xxx']);

$ai->setConfig([
    'search' => [
        'enable'          => true,
        'max_uses'        => 5,               // 单次请求最多搜几次
        'count'           => 10,              // 返回结果条数
        'query'           => 'PHP 8.5 新特性', // 强制指定搜索词,不指定则由模型自己拟
        'recency'         => 'week',          // 时效:hour / day / week / month / year
        'forced'          => true,            // 强制搜索,不让模型自行判断要不要搜
        'citation'        => true,            // 正文里带引用角标
        'sources'         => true,            // 返回搜索来源列表
        'allowed_domains' => ['wikipedia.org'], // 只搜这些域名
        'blocked_domains' => ['spam.com'],      // 不搜这些域名(与上一项互斥)
    ],
]);

  $ai->setConfig(['model' => 'kimi-k2-0905-preview', 'search' => true]);

  $agent = new \Ai\Agent\Agent($ai);                                  // ✅ 用 Agent
  $agent->run([['role' => 'user', 'content' => '今天有哪些重要新闻?']]);
  echo $agent->lastText();
  

print_r(\Ai\Helpers\Protocols::withWebSearch());
// ['claude', 'qwen', 'ernie', 'zhipu', 'moonshot', 'perplexity', 'openrouter']

\Ai\Helpers\Protocols::supportsWebSearch('deepseek');   // false

$ai->setConfig([
    'search'     => ['forced' => true],
    'extra_body' => ['search_options' => ['search_strategy' => 'max']],
]);

use Ai\Tools\HttpFetch;
use Ai\Tools\WebContent;

$fetcher = new HttpFetch(['max_bytes' => 1500 * 1024, 'timeout' => 15]);
$res     = $fetcher->fetch($url);
// $res = ['ok'=>bool, 'status'=>int, 'content_type'=>string, 'final_url'=>string, 'bytes'=>int, 'body'=>string, 'error'=>string]

if ($res['ok']) {
    // 按需渲染成模型友好的格式:text 纯文本 / md Markdown / source 原始源码
    $text = WebContent::render($res['body'], $res['content_type'], 'md', 16000);
}

'fetch_url' => [
    'description'  => '抓取一个公网网页并返回正文,用于查证实时信息。',
    'input_schema' => [
        'type' => 'object',
        'properties' => [
            'url'    => ['type' => 'string'],
            'format' => ['type' => 'string', 'enum' => ['text', 'md', 'source']],
        ],
        '

use Ai\Agent\Memory;

$mem = new Memory(FCPATH . 'writable/agent/memory.md', 20000);  // 第二参数:注入对话时的最大字符数

$block = $mem->forPrompt();          // 读取并截断,空记忆返回 ''
if ($block !== '') {
    $system .= "\n\n# 长期记忆\n" . $block;
}

$mem->append('用户偏好深色主题');    // 追加一条
$mem->write($fullContent);           // 覆盖整份

use Ai\AI;
use Ai\Exceptions\AIException;

// 1. 组装待翻译数据:{ 记录ID: 原文 }
$translateData = [];
foreach ($batch as $rec) {
    $translateData[(int)$rec['id']] = $rec['text_source'];
}

// JSON_UNESCAPED_SLASHES 很关键:否则 </p> 会被转义成 <\/p>,
// 模型会把 \/ 当字面字符照抄进译文,导致译文出现错误转义
$dataJson = json_encode($translateData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);

$prompt = "把下面 JSON 中每个值从 {$from} 翻译成 {$to},"
        . "保持键不变、保留 HTML 标签,只返回 JSON:\n{$dataJson}";

// 2. 调用,带重试与 JSON 校验
$ai = new AI();
$ai->setConfig(['model' => $model, 'api_key' => $apiKey, 'max_tokens' => 1024 * 256])
   ->setTimeout(300);

$content = '';
for ($loop = 1; $loop <= 3; $loop++) {
    try {
        $content = trim($ai->chat($prompt)->getContent());
    } catch (AIException $e) {
        log_message('error', 'AI翻译异常: ' . $e->getMessage());
        break;
    }
    // 去掉模型可能包裹的 

namespace Ai\Models\OpenAI;

use Ai\Models\BaseModel;

class GPT4Turbo extends BaseModel
{
    protected $name     = 'gpt-4-turbo';                        // 发给平台的真实模型名
    protected $platform = 'openai';
    protected $protocol = 'Ai\\Protocol\\OpenAI';               // 复用协议
    protected $endpoint = 'https://api.openai.com/v1/chat/completions';
    protected $features = ['chat', 'stream', 'vision'];
    protected $config   = ['max_tokens' => 4096, 'temperature' => 0.7];
}

namespace Ai\Protocol;

/**
 * 某某云(OpenAI 兼容)
 */
class MyCloud extends OpenAI
{
    public function defaultBaseUrl(): string
    {
        return 'https://api.mycloud.com';
    }

    // 路径与 OpenAI 官方不同时才需要覆盖
    public function chatPath(): string   { return '/v2/chat/completions'; }
    public function modelsPath(): string { return '/v2/models'; }

    // 鉴权方式不是 Authorization: Bearer 时覆盖 buildHeaders()

    /** 常用模型:供后台离线渲染下拉框,也是拉取失败时的兜底 */
    public function knownModels(): array
    {
        return ['my-model-pro' => 'MyCloud Pro'];
    }
}

   AI::create(['model'=>'x', 'protocol'=>'App\\Protocol\\MyApi', 'base_url'=>'https://api.my.com']);
   

$ai = new AI(['api_key' => '...', 'model' => '...']);

$ai->embeddings();   // 文本向量化
$ai->images();       // 图像生成
$ai->audio();        // 语音合成 / 识别(HTTP)
$ai->video();        // 视频生成(异步任务)
$ai->realtime();     // WebSocket 通道,默认关闭

use Ai\Helpers\Capabilities;

if ($ai->supports(Capabilities::IMAGE)) {
    $img = $ai->images()->generate('一只在看书的猫', ['size' => '1024x1024']);
    $paths = $img->saveTo('/var/www/uploads');   // 返回实际写入的绝对路径
}

$ai->capabilities();   // 当前模型支持的能力清单,如 ['embedding', 'image']

$ai = new AI(['api_key' => '...', 'model' => 'text-embedding-3-small']);

// 单条
$vec = $ai->embeddings()->create('这是一段文本')->getVector(0);

// 批量:返回顺序**始终**与输入顺序一致
$res = $ai->embeddings()->create(['第一段', '第二段', '第三段']);
$res->getVectors();      // [[...], [...], [...]]
$res->getVector(1);      // 第二段的向量
$res->getDimensions();   // 1536
count($res);             // 3
$res->getUsage();        // ['prompt_tokens' => .., 'total_tokens' => ..]

$ai->embeddings()->create($texts, [
    'dimensions'      => 512,      // OpenAI text-embedding-3-* 支持降维
    'encoding_format' => 'base64', // 返回 base64 时库会自动解回 float 数组
]);

$res = $ai->embeddings()->create($tenThousandTexts, ['batch_size' => 25]);
// 自动分成 400 个请求,结果按原顺序合并,usage 逐批累加

$ai = new AI(['api_key' => '...', 'model' => 'gpt-image-1']);

$img = $ai->images()->generate('一只在看书的猫', ['size' => '1024x1024', 'n' => 2]);

$img->getUrls();            // ['https://...', 'https://...']
$img->getBase64();          // 平台返回 base64 时在这里
$img->getRevisedPrompt();   // 部分平台会改写提示词,原样回传
count($img);                // 2

// ⚠️ 及时落地:各平台的图片 URL 都有有效期
$paths = $img->saveTo('/var/www/uploads', 'cat');
// ['/var/www/uploads/cat_1.png', '/var/www/uploads/cat_2.png']

$ai->images();  // 子门面
(new \Ai\Protocol\Zhipu())->knownImageModels();   // 取某协议的图像模型清单

$ai = AI::create(['protocol' => 'qwen', 'model' => 'wan2.2-t2i-flash', 'api_key' => '...']);

$task = $ai->images()->generateAsync('一只在看书的猫', ['size' => '1024x1024', 'n' => 2]);
$db->save(['task' => json_encode($task->toArray())]);

// ……稍后
$task = AsyncTask::fromArray(json_decode($row['task'], true), $ai);
if ($task->refresh()->isSucceeded()) {
    $task->getResult()->saveTo('/var/www/uploads');
}

// 整图改写
$ai->images()->edit('/path/cat.png', '把背景换成星空')->saveTo('/var/www/uploads');

// 局部重绘:只改蒙版覆盖的区域
$ai->images()->edit('/path/cat.png', '去掉这只手', ['mask' => '/path/mask.png']);

// 文本 → 音频
$ai = new AI(['api_key' => '...', 'model' => 'gpt-4o-mini-tts']);
$ai->audio()->speech('你好世界')->saveTo('/tmp/hello.mp3');

// 带参数
$audio = $ai->audio()->speech('你好世界', [
    'voice'  => 'sage',    // 音色
    'format' => 'wav',     // 库内统一写 format,各平台字段名不同
    'speed'  => 1.2,
]);
$audio->getBytes();    // 原始音频字节
$audio->getFormat();   // 'wav'
$audio->getSize();     // 字节数

// 音频 → 文本
$text = $ai->audio()->transcribe('/tmp/record.wav', ['language' => 'zh'])->getText();

(new \Ai\Protocol\OpenAI())->knownVoices();
// ['alloy','ash','ballad','coral','echo','sage','shimmer','verse','marin','cedar']

$ai = AI::create([
    'protocol' => 'spark',
    'app_id'   => '<控制台的 APPID>',
    'api_key'  => '<APIKey>:<APISecret>',   // 冒号拼接
]);

// 必须显式启用 WebSocket 通道
$ai->realtime()->useWebSocket()->speech('你好世界')->saveTo('/tmp/hello.mp3');

$text = $ai->realtime()->useWebSocket()->transcribe('/tmp/record.wav')->getText();

// Web 请求里:提交后存库就结束,不阻塞
$task = $ai->video()->generate('日落的海边', ['duration' => 5, 'ratio' => '16:9']);
$db->save(['task' => json_encode($task->toArray())]);

// 定时任务 / 队列 worker 里:恢复并查询
$task = AsyncTask::fromArray(json_decode($row['task'], true), $ai);
if ($task->refresh()->isSucceeded()) {
    $task->getResult()->saveTo('/var/www/videos/x.mp4');
}

// 只在 CLI / worker 里这么用
$task->wait(300, 3);   // 最多等 300 秒,起始间隔 3 秒后指数退避

$task->isTimeout();   // true
$task->isDone();      // false —— 所以 if ($task->isDone()) 的写法天然安全
$task->isFailed();    // false —— 不会被误当失败
$task->getMessage();  // 「任务仍在平台侧处理中……请保存 task_id「xxx」,稍后恢复后再查询」

$img->saveTo('/var/www/uploads');          // 图片,返回路径数组
$audio->saveTo('/tmp/hello.mp3');          // 音频
$video->saveTo('/var/www/v.mp4');          // 视频,默认上限 64MB

$ai->realtime()->useWebSocket()->speech('你好世界');

$ai = new AI([
    'api_key'        => '...',
    'base_url'       => 'https://my-gateway.com/v1',
    'image_endpoint' => 'https://another-host.com/v1/images/generations',  // 可选
]);

class MyProtocol implements ProtocolInterface
{
    use \Ai\Protocol\Concerns\CapabilityDefaults;   // ← 只需加这一行
    // ……原有 6 个方法一字不用改……
}
bash
composer 
bash
php tests/smoke_test.php    # 全部类可加载/可实例化、继承链签名兼容
php tests/stream_test.php   # 40 个协议 × 普通对话 / 流式 / token 统计
php tests/tools_test.php    # 工具调用跨平台一致性
php tests/lib_test.php      # 并发批量 / Memory 并发安全 / 计价 / 日志注入
php tests/cli_test.php      # CLI 参数渲染与命令注入防护
php tests/ssrf_test.php     # SSRF 防护的全部已知绕过向量

composer test               # 依次跑上面全部六套
composer analyse            # PHPStan level 8 静态分析(全绿)
composer compat             # PHP 7.1 兼容性扫描(PHPCompatibility)
composer check              # 上面三样一起跑
"]],
]);

// 3. 解析模型返回的编辑计划
$plan = EditProtocol::parse($response->getContent());

// 4. 校验并执行
$executor = new EditExecutor($workspace->getRoot());   // 越界路径会被拒绝
foreach ($plan->toArray()['actions'] as $a) {
    $action = EditAction::fromArray($a);
    if (!$action->validate()) continue;
    $abs        = $executor->resolveAbsolute($action->file);   // 路径安全解析
    $newContent = $executor->computeContent(file_get_contents($abs), $action);
    file_put_contents($abs, $newContent);                      // 建议先备份
}
 围栏后校验
    $content = preg_replace('@^\s*
\s*$@is', '$2', $content);
    if ($content !== '' && json_decode($content, true) !== null) break;
    $content = '';
}

// 3. 回写
$result = json_decode($content, true);
if (is_array($result)) {
    foreach ($result as $id => $text) {
        if (!isset($translateData[(int)$id]) || trim($text) === '') continue;
        update_translation((int)$id, trim($text));
    }
}