Codex 审计发现 124 个问题,修复 59 个 P0-P2 级别问题: P0 (CRITICAL): - TOCTOU 竞态: TransientRateStore/SseConcurrencyGuard 改用 wp_cache_add 原子锁 - REST 路由: __return_true → check_bearer_present 轻量鉴权 - Schema 验证: messages/tools/tool_choice 添加完整 validate_callback - SQL 列名不匹配: upsert 查询对齐 SchemaManager 定义 - SQL 表名插值: 统一使用 %i 占位符 P1 (HIGH): - IP hash 加盐 (HMAC) + IP 解析统一 (context 共享) - Header 注入防护 (X-Request-Id/响应头 \r\n 过滤) - 数值参数范围校验 (temperature/top_p/penalties/max_tokens) - insert_key 错误处理 + revoke_key 审计日志 - Pipeline finally 保护 + RateLimiter rollback 双 store - wp_unslash 补全 + strtotime 返回值检查 - RedisRateStore $rid 冒号验证 - UpstreamStreamClient catch \Throwable P2 (MEDIUM): - AuditLogRepository DRY (提取 build_where_clause) - Token 估算 CJK 优化 (mb_strlen) - ResponseTransformMiddleware null 检查 - 嵌入响应 usage 数据修复 - SchemaManager autoload 优化 25 files changed, ~530 insertions, ~190 deletions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
198 lines
4.9 KiB
PHP
198 lines
4.9 KiB
PHP
<?php
|
|
/**
|
|
* Response Transformer
|
|
*
|
|
* Converts WPMind internal results into OpenAI-compatible response formats.
|
|
*
|
|
* @package WPMind\Modules\ApiGateway\Transform
|
|
* @since 1.0.0
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace WPMind\Modules\ApiGateway\Transform;
|
|
|
|
/**
|
|
* Class ResponseTransformer
|
|
*
|
|
* Transforms WPMind PublicAPI results into OpenAI-compatible
|
|
* JSON response structures.
|
|
*/
|
|
final class ResponseTransformer {
|
|
|
|
/**
|
|
* Transform a chat result into OpenAI chat.completion format.
|
|
*
|
|
* @param mixed $result Result from PublicAPI::chat().
|
|
* @param string $model The model identifier from the request.
|
|
* @param string $request_id Unique request ID.
|
|
* @return array OpenAI-compatible chat completion response.
|
|
*/
|
|
public function transform_chat( mixed $result, string $model, string $request_id ): array {
|
|
$content = $this->extract_content( $result );
|
|
|
|
return [
|
|
'id' => 'wpmind-' . $request_id,
|
|
'object' => 'chat.completion',
|
|
'created' => time(),
|
|
'model' => $model,
|
|
'choices' => [
|
|
[
|
|
'index' => 0,
|
|
'message' => [
|
|
'role' => 'assistant',
|
|
'content' => $content,
|
|
],
|
|
'finish_reason' => 'stop',
|
|
],
|
|
],
|
|
'usage' => $this->extract_usage( $result ),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Transform an embedding result into OpenAI embedding format.
|
|
*
|
|
* @param mixed $result Result from PublicAPI::embed().
|
|
* @param string $model The model identifier from the request.
|
|
* @param string $request_id Unique request ID.
|
|
* @return array OpenAI-compatible embedding response.
|
|
*/
|
|
public function transform_embedding( mixed $result, string $model, string $request_id ): array {
|
|
$embeddings = $this->extract_embeddings( $result );
|
|
$data = [];
|
|
|
|
foreach ( $embeddings as $index => $embedding ) {
|
|
$data[] = [
|
|
'object' => 'embedding',
|
|
'index' => $index,
|
|
'embedding' => $embedding,
|
|
];
|
|
}
|
|
|
|
return [
|
|
'object' => 'list',
|
|
'data' => $data,
|
|
'model' => $model,
|
|
'usage' => $this->extract_usage( $result ),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Transform a models list into OpenAI models list format.
|
|
*
|
|
* @param string[] $models List of model identifiers.
|
|
* @return array OpenAI-compatible models list response.
|
|
*/
|
|
public function transform_models( array $models ): array {
|
|
$data = [];
|
|
|
|
foreach ( $models as $model_id ) {
|
|
$data[] = [
|
|
'id' => $model_id,
|
|
'object' => 'model',
|
|
'created' => 0,
|
|
'owned_by' => 'wpmind',
|
|
];
|
|
}
|
|
|
|
return [
|
|
'object' => 'list',
|
|
'data' => $data,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Extract text content from a WPMind result.
|
|
*
|
|
* The result may be a plain string, or an array with a 'content' key,
|
|
* or an array with nested 'choices'.
|
|
*
|
|
* @param mixed $result WPMind API result.
|
|
* @return string Extracted content.
|
|
*/
|
|
private function extract_content( mixed $result ): string {
|
|
if ( is_string( $result ) ) {
|
|
return $result;
|
|
}
|
|
|
|
if ( is_array( $result ) ) {
|
|
if ( isset( $result['content'] ) && is_string( $result['content'] ) ) {
|
|
return $result['content'];
|
|
}
|
|
|
|
if ( isset( $result['choices'][0]['message']['content'] ) ) {
|
|
return (string) $result['choices'][0]['message']['content'];
|
|
}
|
|
|
|
if ( isset( $result['text'] ) && is_string( $result['text'] ) ) {
|
|
return $result['text'];
|
|
}
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Extract usage data from a WPMind result.
|
|
*
|
|
* @param mixed $result WPMind API result.
|
|
* @return array{prompt_tokens: int, completion_tokens: int, total_tokens: int}
|
|
*/
|
|
private function extract_usage( mixed $result ): array {
|
|
$default = [
|
|
'prompt_tokens' => 0,
|
|
'completion_tokens' => 0,
|
|
'total_tokens' => 0,
|
|
];
|
|
|
|
if ( ! is_array( $result ) || ! isset( $result['usage'] ) ) {
|
|
return $default;
|
|
}
|
|
|
|
$usage = $result['usage'];
|
|
|
|
return [
|
|
'prompt_tokens' => (int) ( $usage['prompt_tokens'] ?? 0 ),
|
|
'completion_tokens' => (int) ( $usage['completion_tokens'] ?? 0 ),
|
|
'total_tokens' => (int) ( $usage['total_tokens'] ?? 0 ),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Extract embedding vectors from a WPMind result.
|
|
*
|
|
* @param mixed $result WPMind API result.
|
|
* @return array<int, array<float>> List of embedding vectors.
|
|
*/
|
|
private function extract_embeddings( mixed $result ): array {
|
|
if ( ! is_array( $result ) ) {
|
|
return [];
|
|
}
|
|
|
|
// Result is already a list of vectors.
|
|
if ( isset( $result[0] ) && is_array( $result[0] ) ) {
|
|
return $result;
|
|
}
|
|
|
|
// Result has a 'data' key with embeddings.
|
|
if ( isset( $result['data'] ) && is_array( $result['data'] ) ) {
|
|
$vectors = [];
|
|
foreach ( $result['data'] as $item ) {
|
|
if ( isset( $item['embedding'] ) ) {
|
|
$vectors[] = $item['embedding'];
|
|
} elseif ( is_array( $item ) && isset( $item[0] ) ) {
|
|
$vectors[] = $item;
|
|
}
|
|
}
|
|
return $vectors;
|
|
}
|
|
|
|
// Result has an 'embedding' key (single vector).
|
|
if ( isset( $result['embedding'] ) && is_array( $result['embedding'] ) ) {
|
|
return [ $result['embedding'] ];
|
|
}
|
|
|
|
return [];
|
|
}
|
|
}
|