Commit 3bbc8485 authored by platyhouse's avatar platyhouse

# AI 통합 CLI 도구 및 멀티 프로바이더 API 지원 추가

## 새로운 AI CLI 도구

### ptyAIGetMessage.php
- 다중 AI 프로바이더(Anthropic, OpenAI, Google)를 지원하는 CLI 메시지 도구 추가
- `--ai=섹션명` 옵션으로 설정 파일의 섹션 선택 지원
- `--model=모델명` 옵션으로 런타임 모델 오버라이드 지원
- `--anthropic-beta=기능명` 옵션으로 Claude 베타 기능 오버라이드 지원
- `--verbose` 옵션으로 토큰 사용량 및 curl 디버그 정보 출력
- `--json` 옵션으로 JSON 형식 출력 지원
- 프로바이더별 토큰 사용량 추출 함수 구현 (Anthropic, OpenAI, Google 각각)

## AI 설정 관리

### ptyLibrary_PHP/ai/ptyAIConfig.php
- `~/.ptyAIConfig.ini` 설정 파일 기반의 통합 AI 설정 로더 클래스 추가
- 프로바이더별 기본 모델 자동 설정 기능
- `connect()` 메소드로 설정과 클라이언트 인스턴스를 함께 반환
- `anthropic-beta` 설정 지원으로 Claude 베타 기능 활성화 가능

## API 클라이언트 개선

### ptyLibrary_PHP/ai/claude.api.common.model.php
- `setAnthropicBeta()` 메소드 추가로 `anthropic-beta` 헤더 설정 지원
- `setDebug()` 메소드 추가로 curl 요청/응답 디버그 출력 지원
- 디버그 모드에서 API 키는 앞 12자만 마스킹하여 표시

### ptyLibrary_PHP/ai/chatgpt.api.common.model.php
- `setDebug()` 메소드 추가로 curl 요청/응답 디버그 출력 지원
- `isNewModel()` 메소드 추가로 o1, o3, gpt-5 등 신규 모델 감지
- 신규 모델은 `max_completion_tokens` 사용, 기존 모델은 `max_tokens` 사용하도록 분기 처리

### ptyLibrary_PHP/ai/gemini.api.common.model.php
- Google Gemini API 클라이언트 클래스 신규 추가
- `get()`, `getWithSystem()`, `getSimple()` 메소드로 다양한 호출 방식 지원
- 대화 기록 유지 기능 (`keepHistory` 파라미터)
- `systemInstruction`을 통한 시스템 프롬프트 지원
parent 93f54f7a
#!/usr/bin/env php
<?php
/**
* ptyAIGetMessage
*
* AI API를 통해 메시지를 전송하고 응답을 받는 CLI 도구
*
* 설정 파일: ~/.ptyAIConfig.ini
*
* Usage: ./ptyAIGetMessage "메시지" [--ai=섹션명] [--verbose]
*
* 지원 provider:
* - anthropic: Claude API
* - openai: ChatGPT API
* - google: Gemini API
*/
namespace platyFramework;
require_once __DIR__ . '/ptyLibrary_PHP/cli/ptyCliOptionParser.php';
require_once __DIR__ . '/ptyLibrary_PHP/ai/ptyAIConfig.php';
// 인자 파싱
$parsed = ptyCliOptionParser::parse($argv);
$positionalArgs = $parsed['positional'];
$options = $parsed['options'];
$aiSection = $options['ai'] ?? 'default';
$modelOverride = $options['model'] ?? null;
$anthropicBetaOverride = $options['anthropic-beta'] ?? null;
$verbose = isset($options['verbose']);
$jsonOutput = isset($options['json']);
// 도움말 또는 필수 인자 확인
if (empty($positionalArgs) || isset($options['help'])) {
fwrite(STDERR, "사용법: {$argv[0]} \"메시지\" [옵션]\n");
fwrite(STDERR, "\n");
fwrite(STDERR, "옵션:\n");
fwrite(STDERR, " --ai=섹션명 INI 파일 섹션 (기본값: default)\n");
fwrite(STDERR, " --model=모델명 모델 오버라이드\n");
fwrite(STDERR, " --anthropic-beta=기능명 Claude 베타 기능 오버라이드\n");
fwrite(STDERR, " --verbose 상세 정보 출력 (토큰 사용량, curl 등)\n");
fwrite(STDERR, " --json JSON 형식으로 출력\n");
fwrite(STDERR, " --help 도움말 출력\n");
fwrite(STDERR, "\n");
fwrite(STDERR, "모델 예시:\n");
fwrite(STDERR, " anthropic: claude-sonnet-4-5-20250514, claude-3-5-sonnet-20241022, claude-3-opus-20240229\n");
fwrite(STDERR, " openai: gpt-4o, gpt-4-turbo, gpt-3.5-turbo\n");
fwrite(STDERR, " google: gemini-2.0-flash-exp, gemini-1.5-pro, gemini-1.5-flash\n");
fwrite(STDERR, "\n");
fwrite(STDERR, "anthropic-beta 예시:\n");
fwrite(STDERR, " context-1m-2025-08-07, interleaved-thinking-2025-05-14, max-tokens-3-5-sonnet-2024-07-15\n");
fwrite(STDERR, "\n");
// 현재 설정 파일 내용 표시
$configPath = ptyAIConfig::getConfigPath();
if (file_exists($configPath)) {
fwrite(STDERR, "현재 설정 파일: {$configPath}\n");
fwrite(STDERR, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
$sections = parse_ini_file($configPath, true);
if ($sections) {
foreach ($sections as $name => $config) {
$provider = $config['provider'] ?? '(없음)';
$model = $config['model'] ?? ptyAIConfig::getDefaultModel($provider);
$apiKey = isset($config['apiKey']) ? substr($config['apiKey'], 0, 8) . '...' : '(없음)';
$anthropicBeta = $config['anthropic-beta'] ?? null;
fwrite(STDERR, "[{$name}]\n");
fwrite(STDERR, " provider: {$provider}\n");
fwrite(STDERR, " model: {$model}\n");
fwrite(STDERR, " apiKey: {$apiKey}\n");
if ($anthropicBeta) {
fwrite(STDERR, " anthropic-beta: {$anthropicBeta}\n");
}
}
}
fwrite(STDERR, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
} else {
fwrite(STDERR, "설정 파일 없음: {$configPath}\n");
fwrite(STDERR, "\n");
fwrite(STDERR, ptyAIConfig::getConfigExample() . "\n");
}
exit(isset($options['help']) ? 0 : 1);
}
$message = $positionalArgs[0];
try {
// AI 클라이언트 연결
$connection = ptyAIConfig::connect($aiSection);
$client = $connection['client'];
$config = $connection['config'];
// 모델 오버라이드
if ($modelOverride) {
$client->setModel($modelOverride);
$config['model'] = $modelOverride;
}
// anthropic-beta 오버라이드 (anthropic만 해당)
if ($anthropicBetaOverride !== null && $config['provider'] === 'anthropic') {
$client->setAnthropicBeta($anthropicBetaOverride);
$config['anthropic-beta'] = $anthropicBetaOverride;
}
// 디버그 모드 (verbose일 때 curl 정보 출력)
if ($verbose && method_exists($client, 'setDebug')) {
$client->setDebug(true);
}
if ($verbose) {
fwrite(STDERR, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
fwrite(STDERR, "Provider: {$config['provider']}\n");
fwrite(STDERR, "Model: {$config['model']}\n");
fwrite(STDERR, "Section: {$aiSection}\n");
if (!empty($config['anthropic-beta'])) {
fwrite(STDERR, "anthropic-beta: {$config['anthropic-beta']}\n");
}
fwrite(STDERR, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
fwrite(STDERR, "Message: " . mb_substr($message, 0, 100) . (mb_strlen($message) > 100 ? '...' : '') . "\n");
fwrite(STDERR, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
fwrite(STDERR, "API 호출 중...\n");
}
$startTime = microtime(true);
// 메시지 전송 및 응답 받기 (전체 응답)
$response = $client->get($message);
$elapsed = round((microtime(true) - $startTime) * 1000);
if ($response === false) {
fwrite(STDERR, "Error: AI API 호출에 실패했습니다.\n");
exit(1);
}
// 텍스트 추출
$text = $client->extractText($response);
if ($text === false) {
fwrite(STDERR, "Error: 응답에서 텍스트를 추출할 수 없습니다.\n");
if ($verbose) {
fwrite(STDERR, "Response: " . json_encode($response, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n");
}
exit(1);
}
// 토큰 사용량 추출
$usage = extractUsage($response, $config['provider']);
if ($verbose) {
fwrite(STDERR, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
fwrite(STDERR, "응답 시간: {$elapsed}ms\n");
if ($usage) {
fwrite(STDERR, "토큰 사용량:\n");
fwrite(STDERR, " - 입력: {$usage['input']} tokens\n");
fwrite(STDERR, " - 출력: {$usage['output']} tokens\n");
fwrite(STDERR, " - 합계: {$usage['total']} tokens\n");
}
fwrite(STDERR, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
}
// JSON 출력
if ($jsonOutput) {
$output = [
'request' => [
'provider' => $config['provider'],
'model' => $config['model'],
'message' => $message,
],
'response' => [
'message' => $text,
'elapsed_ms' => $elapsed,
'_api_result' => $response,
],
];
// anthropic-beta 추가 (있을 경우만)
if (!empty($config['anthropic-beta'])) {
$output['request']['anthropic-beta'] = $config['anthropic-beta'];
}
// 토큰 정보 추가
if ($usage) {
$output['response']['tokens'] = [
'in' => $usage['input'],
'out' => $usage['output'],
'total' => $usage['total'],
];
}
echo json_encode($output, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n";
} else {
// 응답만 출력 (다른 메시지 없이)
echo $text;
}
} catch (\Exception $e) {
fwrite(STDERR, "Error: " . $e->getMessage() . "\n");
exit(1);
}
/**
* API 응답에서 토큰 사용량 추출
*/
function extractUsage($response, $provider)
{
switch ($provider) {
case 'anthropic':
// Claude: usage.input_tokens, usage.output_tokens
if (isset($response['usage'])) {
$input = $response['usage']['input_tokens'] ?? 0;
$output = $response['usage']['output_tokens'] ?? 0;
return [
'input' => $input,
'output' => $output,
'total' => $input + $output,
];
}
break;
case 'openai':
// ChatGPT: usage.prompt_tokens, usage.completion_tokens, usage.total_tokens
if (isset($response['usage'])) {
return [
'input' => $response['usage']['prompt_tokens'] ?? 0,
'output' => $response['usage']['completion_tokens'] ?? 0,
'total' => $response['usage']['total_tokens'] ?? 0,
];
}
break;
case 'google':
// Gemini: usageMetadata.promptTokenCount, usageMetadata.candidatesTokenCount
if (isset($response['usageMetadata'])) {
$input = $response['usageMetadata']['promptTokenCount'] ?? 0;
$output = $response['usageMetadata']['candidatesTokenCount'] ?? 0;
return [
'input' => $input,
'output' => $output,
'total' => $response['usageMetadata']['totalTokenCount'] ?? ($input + $output),
];
}
break;
}
return null;
}
......@@ -16,6 +16,7 @@ class ChatGPTAPIModel extends model
private $maxTokens = 4096;
private $temperature = 1.0;
private $conversationHistory = [];
private $debug = false;
/**
......@@ -67,6 +68,26 @@ class ChatGPTAPIModel extends model
return $this;
}
/**
* 디버그 모드 설정
* @param bool $debug 디버그 모드 활성화 여부
* @return $this
*/
public function setDebug($debug)
{
$this->debug = $debug;
return $this;
}
/**
* 새 모델인지 확인 (o1, o3, gpt-5 등은 max_completion_tokens 사용)
* @return bool
*/
private function isNewModel()
{
return preg_match('/^(o1|o3|gpt-5|gpt-4\.5)/i', $this->model);
}
/**
* ChatGPT API 호출
* @param string $message 사용자 메시지
......@@ -85,11 +106,17 @@ class ChatGPTAPIModel extends model
// API 요청 데이터 구성
$data = [
'model' => $this->model,
'max_tokens' => $this->maxTokens,
'temperature' => $this->temperature,
'messages' => $messages
];
// 새 모델은 max_completion_tokens 사용
if ($this->isNewModel()) {
$data['max_completion_tokens'] = $this->maxTokens;
} else {
$data['max_tokens'] = $this->maxTokens;
}
// API 호출
$response = $this->_sendRequest($data);
......@@ -130,11 +157,17 @@ class ChatGPTAPIModel extends model
$data = [
'model' => $this->model,
'max_tokens' => $this->maxTokens,
'temperature' => $this->temperature,
'messages' => $messages
];
// 새 모델은 max_completion_tokens 사용
if ($this->isNewModel()) {
$data['max_completion_tokens'] = $this->maxTokens;
} else {
$data['max_tokens'] = $this->maxTokens;
}
return $this->_sendRequest($data);
}
......@@ -156,9 +189,30 @@ class ChatGPTAPIModel extends model
'Authorization: Bearer ' . $this->apiKey
];
$jsonData = json_encode($data);
// 디버그 출력
if ($this->debug) {
fwrite(STDERR, "━━━━━━━━━━━━ CURL REQUEST ━━━━━━━━━━━━\n");
fwrite(STDERR, "URL: {$this->apiUrl}\n");
fwrite(STDERR, "Method: POST\n");
fwrite(STDERR, "Headers:\n");
foreach ($headers as $header) {
if (strpos($header, 'Authorization:') === 0) {
$key = substr($this->apiKey, 0, 12) . '...';
fwrite(STDERR, " Authorization: Bearer {$key}\n");
} else {
fwrite(STDERR, " {$header}\n");
}
}
fwrite(STDERR, "Body:\n");
fwrite(STDERR, " " . json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n");
fwrite(STDERR, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
}
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
......@@ -167,6 +221,23 @@ class ChatGPTAPIModel extends model
$error = curl_error($ch);
curl_close($ch);
// 디버그 출력 (응답)
if ($this->debug) {
fwrite(STDERR, "━━━━━━━━━━━━ CURL RESPONSE ━━━━━━━━━━━━\n");
fwrite(STDERR, "HTTP Code: {$httpCode}\n");
if ($error) {
fwrite(STDERR, "cURL Error: {$error}\n");
}
fwrite(STDERR, "Response:\n");
$prettyResponse = json_decode($response, true);
if ($prettyResponse) {
fwrite(STDERR, " " . json_encode($prettyResponse, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n");
} else {
fwrite(STDERR, " {$response}\n");
}
fwrite(STDERR, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
}
if ($error) {
error_log("ChatGPT API cURL Error: " . $error);
return false;
......
......@@ -17,6 +17,8 @@ class ClaudeAPIModel extends model
private $maxTokens = 4096;
private $temperature = 1.0;
private $conversationHistory = [];
private $anthropicBeta = null;
private $debug = false;
/**
* 생성자
......@@ -71,6 +73,28 @@ class ClaudeAPIModel extends model
return $this;
}
/**
* anthropic-beta 헤더 설정
* @param string|null $betaFeature 베타 기능 이름 (예: context-1m-2025-08-07)
* @return $this
*/
public function setAnthropicBeta($betaFeature)
{
$this->anthropicBeta = $betaFeature;
return $this;
}
/**
* 디버그 모드 설정
* @param bool $debug 디버그 모드 활성화 여부
* @return $this
*/
public function setDebug($debug)
{
$this->debug = $debug;
return $this;
}
/**
* Claude API 호출
* @param string $message 사용자 메시지
......@@ -158,9 +182,36 @@ class ClaudeAPIModel extends model
'anthropic-version: ' . $this->apiVersion
];
// anthropic-beta 헤더 추가
if ($this->anthropicBeta) {
$headers[] = 'anthropic-beta: ' . $this->anthropicBeta;
}
$jsonData = json_encode($data);
// 디버그 출력
if ($this->debug) {
fwrite(STDERR, "━━━━━━━━━━━━ CURL REQUEST ━━━━━━━━━━━━\n");
fwrite(STDERR, "URL: {$this->apiUrl}\n");
fwrite(STDERR, "Method: POST\n");
fwrite(STDERR, "Headers:\n");
foreach ($headers as $header) {
// apiKey는 일부만 표시
if (strpos($header, 'x-api-key:') === 0) {
$key = substr($this->apiKey, 0, 12) . '...';
fwrite(STDERR, " x-api-key: {$key}\n");
} else {
fwrite(STDERR, " {$header}\n");
}
}
fwrite(STDERR, "Body:\n");
fwrite(STDERR, " " . json_encode($data, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n");
fwrite(STDERR, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
}
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_POSTFIELDS, $jsonData);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
......@@ -169,6 +220,23 @@ class ClaudeAPIModel extends model
$error = curl_error($ch);
curl_close($ch);
// 디버그 출력 (응답)
if ($this->debug) {
fwrite(STDERR, "━━━━━━━━━━━━ CURL RESPONSE ━━━━━━━━━━━━\n");
fwrite(STDERR, "HTTP Code: {$httpCode}\n");
if ($error) {
fwrite(STDERR, "cURL Error: {$error}\n");
}
fwrite(STDERR, "Response:\n");
$prettyResponse = json_decode($response, true);
if ($prettyResponse) {
fwrite(STDERR, " " . json_encode($prettyResponse, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n");
} else {
fwrite(STDERR, " {$response}\n");
}
fwrite(STDERR, "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n");
}
if ($error) {
error_log("Claude API cURL Error: " . $error);
return false;
......
<?php
namespace platyFramework;
require_once(__DIR__ . "/../ptycommon/model.php");
/**
* Gemini API Model
* Google Gemini API를 사용하기 위한 클래스
*/
class GeminiAPIModel extends model
{
private $apiKey;
private $model = 'gemini-2.0-flash-exp';
private $apiBaseUrl = 'https://generativelanguage.googleapis.com/v1beta/models';
private $maxTokens = 4096;
private $temperature = 1.0;
private $conversationHistory = [];
/**
* 생성자
* @param string $apiKey Google API 키
*/
public function __construct($apiKey)
{
parent::__construct();
$this->apiKey = $apiKey;
}
/**
* 모델 설정
* @param string $model 사용할 Gemini 모델명
* @return $this
*/
public function setModel($model)
{
$this->model = $model;
return $this;
}
/**
* 최대 토큰 수 설정
* @param int $maxTokens
* @return $this
*/
public function setMaxTokens($maxTokens)
{
$this->maxTokens = $maxTokens;
return $this;
}
/**
* Temperature 설정
* @param float $temperature 0.0 ~ 2.0
* @return $this
*/
public function setTemperature($temperature)
{
$this->temperature = $temperature;
return $this;
}
/**
* 대화 기록 초기화
* @return $this
*/
public function resetConversation()
{
$this->conversationHistory = [];
return $this;
}
/**
* Gemini API 호출
* @param string $message 사용자 메시지
* @param bool $keepHistory 대화 기록 유지 여부
* @return array|false 응답 배열 또는 false
*/
public function get($message, $keepHistory = false)
{
// 현재 메시지를 대화 기록에 추가
$contents = $this->conversationHistory;
$contents[] = [
'role' => 'user',
'parts' => [['text' => $message]]
];
// API 요청 데이터 구성
$data = [
'contents' => $contents,
'generationConfig' => [
'maxOutputTokens' => $this->maxTokens,
'temperature' => $this->temperature,
]
];
// API 호출
$response = $this->_sendRequest($data);
if ($response === false) {
return false;
}
// 대화 기록 유지
if ($keepHistory && isset($response['candidates'][0]['content']['parts'][0]['text'])) {
$this->conversationHistory = $contents;
$this->conversationHistory[] = [
'role' => 'model',
'parts' => [['text' => $response['candidates'][0]['content']['parts'][0]['text']]]
];
}
return $response;
}
/**
* 시스템 프롬프트와 함께 메시지 전송
* @param string $message 사용자 메시지
* @param string $systemPrompt 시스템 프롬프트
* @return array|false
*/
public function getWithSystem($message, $systemPrompt)
{
$contents = [
[
'role' => 'user',
'parts' => [['text' => $message]]
]
];
$data = [
'contents' => $contents,
'systemInstruction' => [
'parts' => [['text' => $systemPrompt]]
],
'generationConfig' => [
'maxOutputTokens' => $this->maxTokens,
'temperature' => $this->temperature,
]
];
return $this->_sendRequest($data);
}
/**
* API 요청 전송
* @param array $data 요청 데이터
* @return array|false
*/
private function _sendRequest($data)
{
// API 호출 타임아웃 방지
set_time_limit(0);
ini_set('max_execution_time', '0');
$url = "{$this->apiBaseUrl}/{$this->model}:generateContent?key={$this->apiKey}";
$ch = curl_init($url);
$headers = [
'Content-Type: application/json'
];
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($error) {
error_log("Gemini API cURL Error: " . $error);
return false;
}
if ($httpCode !== 200) {
error_log("Gemini API HTTP Error: " . $httpCode . " - " . $response);
return false;
}
$result = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
error_log("Gemini API JSON Decode Error: " . json_last_error_msg());
return false;
}
return $result;
}
/**
* 응답에서 텍스트만 추출
* @param array $response API 응답
* @return string|false
*/
public function extractText($response)
{
if (isset($response['candidates'][0]['content']['parts'][0]['text'])) {
return $response['candidates'][0]['content']['parts'][0]['text'];
}
return false;
}
/**
* 간단한 텍스트 응답만 받기
* @param string $message
* @return string|false
*/
public function getSimple($message)
{
$response = $this->get($message);
return $this->extractText($response);
}
}
<?php
/**
* ptyAIConfig
*
* AI 공통 설정 로더
* 설정 파일: ~/.ptyAIConfig.ini
*
* 지원 provider:
* - anthropic: Claude API
* - openai: ChatGPT API
* - google: Gemini API
*/
namespace platyFramework;
require_once __DIR__ . "/claude.api.common.model.php";
require_once __DIR__ . "/chatgpt.api.common.model.php";
require_once __DIR__ . "/gemini.api.common.model.php";
/**
* AI 설정 로더 클래스
*/
class ptyAIConfig
{
private static $configPath = null;
/**
* 설정 파일 경로 반환
*/
public static function getConfigPath()
{
if (self::$configPath === null) {
self::$configPath = getenv('HOME') . '/.ptyAIConfig.ini';
}
return self::$configPath;
}
/**
* 설정 파일에서 섹션 목록 조회
*/
public static function getSections()
{
$configPath = self::getConfigPath();
if (!file_exists($configPath)) {
return [];
}
$config = parse_ini_file($configPath, true);
return $config ? array_keys($config) : [];
}
/**
* AI 설정 로드
*
* @param string $section INI 파일 섹션명 (기본값: default)
* @return array 설정 배열 [provider, model, apiKey]
* @throws \Exception 설정 파일이나 섹션이 없을 경우
*/
public static function load($section = 'default')
{
$configPath = self::getConfigPath();
if (!file_exists($configPath)) {
throw new \Exception("AI 설정 파일을 찾을 수 없습니다: {$configPath}\n\n" . self::getConfigExample());
}
$config = parse_ini_file($configPath, true);
if ($config === false) {
throw new \Exception("AI 설정 파일을 파싱할 수 없습니다: {$configPath}");
}
if (!isset($config[$section])) {
$availableSections = implode(', ', array_keys($config));
throw new \Exception("AI 설정에서 [{$section}] 섹션을 찾을 수 없습니다.\n사용 가능한 섹션: {$availableSections}");
}
$sectionConfig = $config[$section];
// 필수 필드 검증
$requiredFields = ['provider', 'apiKey'];
foreach ($requiredFields as $field) {
if (!isset($sectionConfig[$field]) || empty($sectionConfig[$field])) {
throw new \Exception("AI 설정 [{$section}] 섹션에 필수 필드 '{$field}'가 없습니다.");
}
}
$provider = strtolower($sectionConfig['provider']);
$validProviders = ['anthropic', 'openai', 'google'];
if (!in_array($provider, $validProviders)) {
throw new \Exception("AI 설정 [{$section}] 섹션의 provider '{$provider}'는 지원되지 않습니다.\n지원되는 provider: " . implode(', ', $validProviders));
}
// anthropic-beta 값 처리 (베타 기능 이름)
$anthropicBeta = null;
if (isset($sectionConfig['anthropic-beta']) && !empty($sectionConfig['anthropic-beta'])) {
$anthropicBeta = trim($sectionConfig['anthropic-beta'], '"\'');
}
return [
'provider' => $provider,
'model' => $sectionConfig['model'] ?? self::getDefaultModel($provider),
'apiKey' => trim($sectionConfig['apiKey'], '"\''),
'anthropic-beta' => $anthropicBeta,
];
}
/**
* provider별 기본 모델 반환
*/
public static function getDefaultModel($provider)
{
$defaults = [
'anthropic' => 'claude-3-5-sonnet-20241022',
'openai' => 'gpt-4o',
'google' => 'gemini-2.0-flash-exp',
];
return $defaults[$provider] ?? null;
}
/**
* AI 클라이언트 인스턴스 생성
*
* @param string $section INI 파일 섹션명 (기본값: default)
* @return ClaudeAPIModel|ChatGPTAPIModel|GeminiAPIModel
* @throws \Exception
*/
public static function createClient($section = 'default')
{
$config = self::load($section);
switch ($config['provider']) {
case 'anthropic':
$client = new ClaudeAPIModel($config['apiKey']);
$client->setModel($config['model']);
if (!empty($config['anthropic-beta'])) {
$client->setAnthropicBeta($config['anthropic-beta']);
}
return $client;
case 'openai':
$client = new ChatGPTAPIModel();
$client->setAPIKey($config['apiKey']);
$client->setModel($config['model']);
return $client;
case 'google':
$client = new GeminiAPIModel($config['apiKey']);
$client->setModel($config['model']);
return $client;
default:
throw new \Exception("지원되지 않는 provider: {$config['provider']}");
}
}
/**
* 설정 정보와 함께 AI 클라이언트 반환
*
* @param string $section INI 파일 섹션명 (기본값: default)
* @return array [client => object, config => array]
*/
public static function connect($section = 'default')
{
$config = self::load($section);
$client = self::createClient($section);
return [
'client' => $client,
'config' => $config,
];
}
/**
* 설정 파일 예시 반환
*/
public static function getConfigExample()
{
return <<<EOT
설정 파일 예시 (~/.ptyAIConfig.ini):
[default]
provider=anthropic
model=claude-sonnet-4-5-20250514
apiKey=your_anthropic_api_key
[claude-beta]
provider=anthropic
model=claude-sonnet-4-5-20250514
apiKey=your_anthropic_api_key
anthropic-beta=context-1m-2025-08-07
[openai]
provider=openai
model=gpt-4o
apiKey=your_openai_api_key
[google]
provider=google
model=gemini-2.0-flash-exp
apiKey=your_google_api_key
# 지원 provider: anthropic, openai, google
# anthropic-beta: Claude 베타 기능 사용 시 헤더값 (예: context-1m-2025-08-07)
EOT;
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment