Prism PHP에 새로운 LLM 제공업체 추가하기: Z.AI 지원 구현 방법
요약
Laravel 패키지인 Prism PHP에 새로운 LLM 제공업체인 Z.AI를 추가하는 과정을 상세히 설명합니다. 제공업체 클래스, 핸들러, 맵의 구조를 통해 새로운 모델을 통합하는 구체적인 구현 단계를 다룹니다.
핵심 포인트
- Prism PHP의 3가지 핵심 요소(Provider, Handler, Map) 구조 이해
- Z.AI 지원을 위한 Enum 등록 및 설정 방법
- 제공업체 클래스 구현 및 보안을 위한 SensitiveParameter 활용
- 메시지 및 도구 호출을 위한 매핑 로직 구현 가이드
Prism은 하나의 유연한 API (fluent API)로 LLM과 통신할 수 있게 해주는 Laravel 패키지입니다. OpenAI, Anthropic, Gemini, Groq, Mistral, xAI, Perplexity 등을 지원합니다. 하지만 GLM 모델의 제공업체인 Z.AI는 포함되어 있지 않았기에, 이를 추가하기 위해 PR을 오픈했습니다.
해당 PR은 현재 병합되었습니다: prism-php/prism#794. 28개의 파일, 약 1,500줄의 코드, 텍스트 생성 (text generation), 도구 호출 (tool calling), 멀티모달 입력 (multimodal input) 및 구조화된 출력 (structured output)을 포함합니다.
이 포스트는 여러분이 직접 자신만의 제공업체를 추가하고 싶을 때 실제로 필요한 부분들로 해당 PR을 세분화하여 설명합니다.
멘탈 모델 (The mental model)
Prism은 세 가지 요소를 분리합니다:
- 제공업체 클래스 (The provider class): HTTP 클라이언트를 소유하며 그 외의 역할은 하지 않습니다.
- 핸들러 (Handlers): 각 기능(
text,structured,embeddings, ...)당 하나씩 존재합니다. 핸들러는 페이로드 (payload)를 구축하고, 이를 전송하며, 원시 JSON을 Prism 값 객체 (value objects)로 변환합니다. - 맵 (Maps): 하나의 Prism 개념을 하나의 제공업체 개념으로 번역하는 아주 작은 상태가 없는 (stateless) 클래스들입니다. 메시지 (Messages), 도구 (tools), 도구 선택 (tool choice), 종료 사유 (finish reasons), 미디어 (media) 등이 해당됩니다.
저장소의 모든 제공업체는 이 구조를 따릅니다. 이 구조를 복사하면 리뷰가 빠르게 진행되지만, 자신만의 방식을 발명하면 그렇지 않습니다.
1단계: 제공업체 등록하기
세 가지 작은 수정을 통해 Prism이 Provider::Z를 해석할 수 있게 됩니다.
Enum:
enum Provider: string
{
case Z = 'z';
...
config/prism.php 내의 설정:
'z' => [
'url' => env('Z_URL', 'https://api.z.ai/api/coding/paas/v4'),
'api_key' => env('Z_API_KEY', ''),
...
그리고 PrismManager의 팩토리 메서드 (factory method). 매니저는 관례에 따라 create{Name}Provider를 찾아내므로, 메서드 이름 자체가 전체 연결 과정입니다:
protected function createZProvider(array $config): Z
{
return new Z(
...
2단계: 제공업체 클래스
Z는 Prism의 추상 Provider를 확장하며, 어떤 핸들러가 실행될지만 결정합니다. API 키가 스택 트레이스 (stack trace)에 유출되지 않도록 API 키에 적용된 #[\\SensitiveParameter]를 확인하세요:
class Z extends Provider
{
use InitializesClient;
...
검토 과정에서 변경한 한 가지 사항은 final 키워드를 제거하고 client()와 인코더(encoder) 메서드들을 protected로 변경한 것입니다. 사람들은 게이트웨이(gateway)나 프록시(proxy)를 가리키기 위해 프로바이더(provider)를 확장하곤 합니다. 클래스를 봉인(sealing)하는 것은 아무런 이득 없이 이를 차단하는 행위입니다.
3단계: 메시지 매핑 (mapping messages)
이 부분이 프로바이더들이 실제로 차이를 보이는 지점입니다. Z.AI는 OpenAI 형태의 채팅 API (chat API)를 사용하지만, 미디어(media) 부분은 독자적인 방식을 따릅니다.
MessageMap은 대화 앞에 시스템 프롬프트 (system prompts)를 병합한 다음, 메시지 클래스에 따라 분기(dispatch)합니다:
protected function mapMessage(Message $message): void
{
match ($message::class) {
...
흥미로운 부분은 사용자 메시지(user message)입니다. Z.AI는 이미지, 파일, 비디오를 하나의 일반적인 첨부 파일(attachment)이 아닌, 별도의 타입이 지정된 콘텐츠 파트(typed content parts)로 받기 때문입니다:
protected function mapUserMessage(UserMessage $message): void
{
$images = array_map(fn (Media $media): array => (new DocumentMapper($media, DocumentType::ImageUrl))->toPayload(), $message->images());
...
세 가지 미디어 타입은 하나의 매퍼 (mapper)와 열거형 (enum)으로 통합됩니다. 키(key)를 제외하면 페이로드 (payload) 형태가 동일하기 때문입니다:
enum DocumentType: string
{
case FileUrl = 'file_url';
...
validateMedia()는 $this->media->isUrl()을 반환하여 제약 조건을 명시적으로 만듭니다. 즉, 이 엔드포인트 (endpoint)는 base64 블롭 (blobs)이 아닌 URL을 허용한다는 의미입니다. Prism은 API가 모호한 400 에러를 반환하는 대신 명확한 예외 (exception)를 발생시킵니다.
4단계: 도구 (tools)
두 개의 매핑이 더 있습니다. ToolMap은 Prism의 Tool을 함수 정의 (function definition)로 변환하며, ToolChoiceMap은 세 가지 강제 모드 (forcing modes)를 처리합니다:
public static function map(string|ToolChoice|null $toolChoice): string|array|null
{
if (is_null($toolChoice)) {
...
문자열은 "이 특정 도구를 호출하라"는 의미입니다. ToolChoice::Any는 required가 됩니다. 그 외의 모든 것은 API에 의해 조용히 전송되어 무시되는 대신, 명시적인 오류를 내며 거부됩니다.
5단계: 텍스트 핸들러 (text handler) 및 에이전트 루프 (agent loop)
핸들러는 요청을 보내고, 종료 사유 (finish reason)에 따라 분기합니다. 이것이 전체 멀티 스텝 도구 루프 (multi step tool loop)입니다:
public function handle(Request $request): TextResponse
{
$response = $this->sendRequest($request);
...
handleToolCalls는 도구 (tools)를 실행하고, ToolResultMessage를 추가하며, 단계를 기록한 뒤, steps->count() < $request->maxSteps()인 동안 재귀적으로 호출합니다. 단계 제한 (step cap)은 요청 (request)에 포함되어 있으므로, 제어 불능 상태의 에이전트 (agent)가 제공업체 (provider)가 아닌 사용자의 설정에 따라 멈추게 됩니다.
참고할 만한 두 가지 방어적 기법 (defensive touches)은 다음과 같습니다:
- 종료 사유 (finish reason)가
tool_calls라고 되어 있지만 배열이 비어 있는 경우, 예외를 발생시킵니다. 잘못된 형식의 응답이 조용한 빈 답변으로 이어져서는 안 됩니다. - 매핑되지 않은 종료 사유가 발생할 경우에도 "stop"으로 넘어가는 대신 예외를 발생시킵니다. 원래는 실수로 xAI의 종료 사유 맵 (finish reason map)을 재사용했는데, 리뷰 커밋 중 하나가 바로 이 부분을 수정하는 것이었습니다.
페이로드 (Payload) 생성에는 Arr::whereNotNull을 사용하여, 선택적 파라미터 (optional parameters)가 설정되지 않은 경우 단순히 사라지도록 처리합니다:
$payload = array_merge([
'model' => $request->model(),
'messages' => (new MessageMap($request->messages(), $request->systemPrompts()))(),
...
6단계: 네이티브 스키마 (native schema) 지원 없는 구조화된 출력 (structured output)
이것이 이번 PR (Pull Request)에서 내린 유일한 실제 설계 결정이었습니다.
OpenAI는 엄격한 JSON 스키마 (JSON schema) 모드를 가지고 있습니다. Z.AI의 코딩 엔드포인트 (coding endpoint)는 response_format: json_object를 지원하며, 이는 유효한 JSON은 보장하지만 사용자의 JSON을 보장하지는 않습니다. 따라서 스키마를 마지막 시스템 메시지 (system message)로 전달하며, StructuredMap은 MessageMap을 확장하여 이를 추가하기만 합니다:
class StructuredMap extends MessageMap
{
public function __construct(array $messages, array $systemPrompts, private readonly Schema $schema)
...
또한 요청 시 thinking: ['type' => 'disabled']를 전송합니다. 추론 흔적 (reasoning traces)이 JSON 페이로드로 유출되는 것은 여기서 결코 원치 않는 실패 모드이기 때문입니다.
메시지 맵 (message map)을 복사하는 대신 서브클래싱 (Subclassing)함으로써, 멀티모달 (multimodal) 구조화된 요청이 별도의 작업 없이도 계속 작동하게 됩니다.
7단계: 기록된 픽스처 (fixtures)를 사용한 테스트
Prism은 라이브 API가 아닌 기록된 HTTP 응답을 대상으로 제공업체(providers)를 테스트합니다. FixtureResponse::fakeResponseSequence는 번호가 매겨진 시퀀스를 재생하며, 이것이 다단계 도구(multi-step tool) 테스트를 가능하게 하는 핵심입니다.
it('can generate text using multiple tools and multiple steps', function (): void {
FixtureResponse::fakeResponseSequence('chat/completions', 'z/generate-text-with-multiple-tools');
...
일반 프롬프트, 시스템 프롬프트, 병렬 도구 호출(parallel tool calls), 강제 도구 호출(forced tool call), 429 속도 제한(rate limit), 이미지, 파일 및 비디오 URL, 그리고 구조화된 출력(structured output)을 포함하여 총 9개의 픽스처(fixtures)가 있습니다. 저의 나중 커밋 중 하나는 말 그대로 patch(z): remove token이었으므로, 기록된 픽스처를 커밋하기 전에 반드시 데이터를 삭제(scrub)하세요.
사용 방법
$response = Prism::text()
->using('z', 'glm-4.6')
->withPrompt('Write a short story about a robot learning to love')
...
glm-4.6v를 사용한 멀티모달(Multimodal):
$response = Prism::text()
->using('z', 'glm-4.6v')
->withMessages([
...
Z_API_KEY를 설정하면 완료됩니다.
과거의 나에게 해주고 싶은 말
먼저 인접한 제공업체를 읽어보세요. 기존 제공업체들을 따라 함으로써 구조를 올바르게 잡을 수 있었고, 리뷰 코멘트는 아키텍처가 아닌 세부 사항에 관한 것이었습니다.
지도를 읽지 않고 복사하지 마세요. xAI의 종료 사유(finish reason) 맵은 문제없이 컴파일되었지만 틀렸었습니다.
문서(Docs)도 PR의 일부입니다. docs/providers/ 아래의 제공업체 페이지와 VitePress 설정의 사이드바 항목을 포함해야 합니다. 유지 관리자가 이를 요청하게 해서는 안 됩니다.
머지(merge)까지 시간이 걸릴 것을 예상하세요. 저는 12월에 이 PR을 열었고, 유지 관리자가 포맷팅 및 리팩터링 커밋을 추가하면서 3월에 머지되었습니다. 모든 제공업체가 장기적인 유지 관리 약속이 되는 저장소에서는 이것이 정상입니다.
전체 diff는 GitHub에서 확인할 수 있습니다: prism-php/prism#794.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기