MMSP 개발 워크플로우
모델 지원을 추가하거나 업데이트하는 작업은 아래 단계를 순서대로 따릅니다. 단계에 멈추고 물어보기라고 표시된 경우, 사용자에게 질문하고 문제가 해결될 때까지 중단하며, 직접 빈칸을 채우지 마십시오.
디렉토리 맵
llmsdk_docs/<model_version>/ 공식 문서 스냅샷, 모델 세대당 하나의 폴더 (README.md + docs/)
api_captures/<protocol>/ Git에서 무시되는 원시 API 캡처: 요청 페이로드 + 스트림 이벤트
src_py/mmsp/<client_type>/ Python 클라이언트, 클라이언트 유형당 하나의 폴더: <vendor>_official/ 또는 <protocol>/
src_py/mmsp/auto_client.py client_type 이름으로 클라이언트를 생성; 모델 ID의 패밀리가 공식 클라이언트 이름을 결정
src_ts/src/<client_type>/ TypeScript 클라이언트, Python 폴더를 미러링
src_ts/src/autoClient.ts TypeScript 라우팅, auto_client.py 미러링
src_py/tests/test_client.py 매개변수화된 E2E 테스트 (환경 변수로 게이트된 AVAILABLE_MODELS)
src_ts/tests/client.test.ts TypeScript용 동일
changelog/unreleased/ 출시되지 않은 변경 사항이 저장되는 곳; 릴리스 시 버전 이름으로 변경
changelog/<version>/ 릴리스 요약 (README.md) 및 각 항목당 하나의 상세 파일
changelog/README.md 항목 형식: 메타데이터 블록, 본문 규칙, 이중 언어 페어링
CHANGELOG.md 각 릴리스당 한 줄 요약, changelog/로 링크
src_py/mmsp/integration/ 플레이그라운드, 트레이서 페이지 및 MMSP 서버, src_ts/src/integration/에 미러링
site/ mmsp.penguin.ooo의 Astro 사이트
.agents/skills/mmsp-dev/kill-ai-slop/ 아래 UI 작업을 위한 공급된 AI 슬롭 가이드 및 스캐너
<name>.zh.md 중국어 대응 파일, changelog/의 모든 파일에 필요
1단계 — 공식 문서를 llmsdk_docs/에 동기화
- 소스는 모델 공급업체의 공식 문서 사이트여야 합니다 (예: docs.anthropic.com, platform.openai.com, ai.google.dev). 타사 미러, 블로그 게시물, 또는 모델 메모리를 절대 사용하지 마십시오.
llmsdk_docs/<model_version>/아래에 기존 폴더 규칙을 따라 스냅샷을 저장하고,llmsdk_docs/README.md에 폴더를 나열하십시오. 이 워크플로우를 실행하는 것은llmsdk_docs/편집을 금지하는 저장소 규칙에 대한 명시적 요청입니다.- 가져온 문서가 기존 스냅샷과 다른 경우, 새 공식 문서가 우선합니다: 이전 파일을 그 자리에서 업데이트하십시오.
- 스냅샷은 구현하기에 충분히 완전해야 합니다: 요청/응답 스키마, 스트리밍 이벤트 시퀀스, 생각 출력, 도구 호출, 사용량 필드, 오류 응답.
- 멈추고 물어보기 공식 URL이 불확실하거나 페이지를 가져올 수 없는 경우. 사용자가 수동으로 내용을 붙여넣을 수 있습니다.
2단계 — 실시간 API 교환을 api_captures/에 캡처
- 게이트: 제공자의 API 키 환경 변수가 설정되고 사용 가능해야 합니다.
src_py/tests/test_client.py(AVAILABLE_MODELS게이팅 및_create_client)와 동일한 환경 변수 및 기본 URL을 사용하십시오. 멈추고 물어보기 키가 누락된 경우 사용자에게 키를 제공하도록 요청하십시오; 키 없이 워크플로우를 계속해서는 안 됩니다. - 제공자의 공식 SDK, 또는 문서에 정확히 설명된 대로 원시 HTTP를 사용하여, 생각 기능이 활성화된 하나의 스트리밍 도구 호출 요청을 실행한 다음, 도구 결과를 다시 보내어 보조 턴이 어떻게 재전송되는지 보여주십시오.
- 전체 교환을 수정하지 않고
api_captures/<protocol>/아래에 저장하십시오 (git-ignored), 예:round1.request.json과 모든 원시 스트림 이벤트를 순서대로 포함하는round1.stream.jsonl. 자격 증명을 절대 저장하지 마십시오. - 교환 중 일부를 JSON으로 직렬화할 수 없는 경우 (바이너리 페이로드, 비-JSON 값을 보유하는 SDK 객체),
str()형태를 저장하고 그로부터 분석하십시오. 컨테이너를 구문 분석 가능하게 유지하십시오: 텍스트를 JSON 객체로 감싸.jsonl이 한 줄에 하나의 JSON 값으로 유지되도록 하거나 ({"unserializable_str": "<str(event)>"}, 이벤트를 스트림 순서대로 유지), 직렬화할 수 있는 것이 전혀 없을 때 전체 교환을 형제.txt파일에 넣으십시오.str()캡처는 여전히 API가 반환한 것의 권위 있는 기록입니다. 이벤트를 버리거나, 유효한 JSON으로 수동 편집하거나, 직렬화 실패로 인해 문서로 대체하지 마십시오. - 멈추고 물어보기 API 오류 (유효하지 않은 키, 할당량 부족, 속도 제한)가 발생한 경우. 응답을 모의하거나 문서만으로 계속하지 마십시오.
- 캡처는 기본 구현 참조이며 문서보다 우선합니다: 둘이 일치하지 않는 경우, API가 실제로 반환한 것을 구현하십시오.
- 그런 다음 재생을 프로브하십시오. 캡처는 API가 보내는 것만 보여주지, 되받아야 하는 것은 보여주지 않기 때문입니다. 보조 턴을 반복적으로 재전송하되, 매번 하나의 후보 필드를 제거하십시오 — 추론 항목의 id, 그 내용, 전체 추론 항목, 유지하고 싶었던 각 필드 — 어떤 제거가 모델의 동작을 손상시키지 않고 API에서 여전히 수락되는지 기록하십시오. 그 결과가, 응답의 형태가 아니라, 3단계가 저장할 내용을 결정합니다. 프로브 결과를 캡처 옆에 저장하십시오.
3단계 — Python 및 TypeScript 클라이언트 구현
- 클라이언트 유형당 하나의 폴더, 그 이름을 따서 명명: 공급업체당 공식 클라이언트 (
openai_official/,anthropic_official/,google_official/,zai_official/,moonshot_official/,deepseek_official/,minimax_official/)는 공급업체 자체 API를 사용하며, 공급업체 모델의 모든 세대를 알고, 환경에서 공급업체의 키를 읽습니다; 와이어 프로토콜당 호환 클라이언트 (openai_responses/,openai_chat/,openai_chat_vllm_adapter/,openai_embedding/,ant_messages/,google_genai/,mmsp/)는 모든 엔드포인트에 대해 해당 프로토콜을 사용합니다. 클래스는 폴더 이름을 따서 명명되고 (OpenAIOfficialClient,OpenaiChatClient), 클라이언트 유형은 하이픈이 있는 폴더 이름입니다 (openai-official,openai-chat).- 공급업체 모델의 새 세대는 공급업체의 공식 클라이언트에 들어갑니다. 새 프로토콜(캡처 + 문서)을 클라이언트가 오늘 보내는 것과 비교하십시오; 세대가 다른 경우, 단일 키 이름이라도, 명시적 버전 (
"4-6" in self._model,"gpt-6" in self._model)으로 클라이언트 내에서 세대를 구분하고,"claude" in model과 같은 베어 하위 문자열로 절대 구분하지 마십시오, 그리고 이전 세대가 계속 작동하도록 유지하십시오. 사용자가 명시적으로 지시하지 않는 한 세대를 절대 삭제하지 마십시오. - 새 공급업체는 새 공식 클라이언트 폴더,
auto_client.py/autoClient.ts의 새<vendor>-official클라이언트 유형, 그리고 거기의 모델 패밀리 테이블에 새 행을 얻습니다: 패밀리는 공급업체 모델의 모든 ID가 시작하는 접두사입니다 (glm-,kimi-), 대소문자를 구분하지 않고 일치. - 여러 공급업체가 제공하는 새 와이어 프로토콜은 호환 클라이언트 폴더와 클라이언트 유형을 얻습니다; 호환 클라이언트는 공급업체별 특성을 지니지 않습니다.
- 공급업체 모델의 새 세대는 공급업체의 공식 클라이언트에 들어갑니다. 새 프로토콜(캡처 + 문서)을 클라이언트가 오늘 보내는 것과 비교하십시오; 세대가 다른 경우, 단일 키 이름이라도, 명시적 버전 (
auto_client.py/autoClient.ts는client_type이 이름을 지정하는 클라이언트를, 클라이언트 유형의 하나의 테이블에서 생성합니다.client_type없이, 모델 ID가 시작하는 패밀리가 해당 공식 클라이언트의 이름을 지정합니다; 알려진 패밀리가 없는 ID는 오류를 발생시키고 하나를 요청합니다. 라우팅에는 다른 규칙이 없습니다: 모델 ID에 대한 하위 문자열 일치, 공식 클라이언트 외부의 버전 일치가 없습니다.- 자격 증명은 제공된 곳에만 사용됩니다. 이것이 보안 모델이며, 모든 클라이언트 생성자가 따릅니다.
api_key로 전달된 키는 그대로 사용됩니다. 환경의 일반 키를 읽는 클라이언트 (openai_official및 OpenAI 프로토콜 호환 클라이언트의 경우OPENAI_API_KEY,anthropic_official및ant_messages의 경우ANTHROPIC_API_KEY,google_genai의 경우GEMINI_API_KEY)는 엔드포인트도 환경에서 올 때만 읽습니다 (OPENAI_BASE_URL/ANTHROPIC_BASE_URL/GEMINI_BASE_URL, 또는 공급업체 자체 기본값):base_url이 주어지고api_key가 없으면, 해당 키를 다른 엔드포인트로 보내는 대신 생성 시 오류를 발생시킵니다. 이것이utils의resolve_credentials/resolveCredentials입니다; 호출하되, 재구현하지 마십시오. 다른 공식 클라이언트 (deepseek_official,zai_official,moonshot_official,minimax_official,google_official)는 주어진 기본 URL에 관계없이 자체 변수 (DEEPSEEK_API_KEY,ZAI_API_KEY,MOONSHOT_API_KEY,MINIMAX_API_KEY,GEMINI_API_KEY)를 읽으며, 일반 키는 절대 읽지 않습니다; 둘 중 하나 없이 오류를 발생시킵니다:<VAR> is required for <Client>.모든 자격 증명을 공급업체 SDK에 명시적으로 전달하십시오, "없음"을 포함하여 (TypeScript에서authToken: null, Python에서 지워진auth_token), SDK는 전달되지 않은 것을 자체 환경 변수에서 채우기 때문입니다.tests/env-credentials.test.ts/tests/test_env_credentials.py의 오프라인 테스트는 제어된 환경에서 모든 클라이언트를 빌드하고 SDK 인스턴스가 보유한 자격 증명을 읽습니다; 새 클라이언트를 추가하십시오. - 재생은 최소화해야 하며, 완전할 필요는 없습니다. 재생된 보조 턴은 모델의 동작을 손상시키지 않고 API에서 수락되어야 합니다; 와이어 항목을 필드 대 필드로 복제할 필요는 없습니다.
fidelity는 페이로드의 미러가 아니라 예외 채널입니다: 필드는 2단계 프로브에서 요청이 실패하거나 모델이 저하되는 경우에만 그곳에 자리를 얻습니다. 제공자가 생성한 ID(추론 항목 ID, 출력 항목 ID)는 API가 재생성하거나 무시하므로 제외됩니다. API가 요구하는 필드는 포함됩니다: GPT-5.5의phase, Claude의 생각signature, 엄격한 업스트림이 요구하는 정확한 추론 필드 이름. - 보편적 필드가 이미 전달하는 것을
fidelity에 복사하지 마십시오 — 생각 텍스트, 도구 호출의 이름 또는 구문 분석된 인수. 대신 보편적 필드에서 와이어 항목을 재구축하십시오. tool_call_id는 선택 사항이 아닙니다: 항상 제공자의 호출 ID를 캡처하고 재생하십시오. 와이어 형식이 항목 ID와 호출 ID를 모두 전달하는 경우, 호출 ID가 결과를 해당 호출과 상호 연관시키는 것입니다.- 참조 클라이언트를 따르고, 재설계하지 마십시오.
openai_official/는 Responses 스타일 프로토콜의 형태이고,openai_chat/는 Chat Completions의 형태입니다: 동일한 메서드 순서, 동일한 제어 흐름, 동일한 이름. 새 클라이언트는 참조 클라이언트에 대한 diff로 읽혀야 합니다, 그렇게 검토될 것이기 때문입니다. - 위에서 아래로 읽기 쉽게 유지하십시오. 독자는 점프 없이 파일을 통해 하나의 요청 또는 응답을 곧바로 따라갈 수 있어야 하므로, 인라인이 기본값입니다: SDK의 속성을 직접 읽고 (
model_output.delta,model_output.item.call_id), 필드 접근, 사용량 산술, 오류 텍스트를 사용되는 곳에 유지하십시오. 진정으로 크고 자체 포함된 블록(인라인하면 메인 흐름을 묻을 종류, 예: 이미지 가져오기 및 디코딩)에 대해서만 헬퍼를 추출하고, 몇 줄에 대해서는 절대 추출하지 마십시오. 참조 클라이언트의 자체 비공개 메서드 (_convert_thinking_level_*,_convert_tool_choice)를 미러링하고, 그 옆에 계층을 발명하지 마십시오; dict와 SDK 객체를 모두 받는 shim은 절대 그 중 하나가 아닙니다, 이벤트는 SDK 자체 유형으로 타입이 지정되기 때문입니다. - 클라이언트는 델타만 생성합니다; 기본 클래스가 항목을 닫습니다.
transform_model_output_to_uni_event/transformModelOutputToUniEvent는 하나의 와이어 이벤트를 하나의UniEvent로 변환하며, 그content_items는 해당 이벤트가 전달하는.delta항목을 와이어 순서대로 포함하고, 절대.done항목을 포함하지 않습니다._streaming_response_internal/_streamingResponseInternal은 요청을 빌드하고, 스트림을 열고, 와이어 이벤트당 하나의 이벤트를 생성합니다: 항목 관리, 누산기, 인수 구문 분석, 사용량 병합, 합성된 중지 없이 단순한 루프입니다.usage_metadata/finish_reason은 와이어가 이를 보고하는 모든 곳에서stop이벤트에 탑니다 — Anthropicmessage_start및message_delta, Chat Completions 완료 및 사용량 청크,response.completed— 기본 클래스에 의해 필드별로 병합됩니다; 클라이언트의stop이벤트는 공개 스트림을 종료하지 않습니다. 보편적인 것이 없는 와이어 이벤트는 빈 델타 이벤트입니다: 항목 없음, null 사용량 및 완료 이유. 기본 클래스는 모든 델타를StreamItems(stream_items.py/streamItems.ts)에 전달하며, 이는 도착하는 대로 델타를 내보내고 다음 항목이 시작되거나 클라이언트 스트림이 종료될 때 각 항목을.done항목으로 닫습니다; 그런 다음 기본 클래스는 하나의 stop 이벤트를 내보내거나EmptyResponseError를 발생시킵니다. 프로토콜을 위반하는 이벤트는 모든 모드에서StreamProtocolError를 발생시키며, 공유 E2E 테스트는assert_stream_grammar/assertStreamGrammar로 모든 클라이언트의 스트림을 확인합니다. - 하나의 항목에 대한 델타는 연속적이며 하나의
fidelity.item_id를 공유합니다. 모델 출력은 직렬이므로, 한 번에 하나의 항목만 스트리밍되며 제공자의 항목 종료 이벤트 (content_block_stop,response.output_item.done,step.stop)는 no-op으로 나열됩니다: 항목을 늦게 또는 순서 없이 닫는 게이트웨이는 아무것도 변경하지 않습니다. 제공자의 자체 ID를 문자열화하여fidelity.item_id로 전달하십시오: Anthropic 블록index, Responses 이벤트item_id또는item.id 또는 item.call_id(게이트웨이가 보내지 않을 때None/undefined), Interactions 단계index, Chat Completions 와이어 필드 (reasoning_content,reasoning,content,tool_calls), generateContent 부품 종류 (function_call,thought,inline_thinking,inline_data,text); 임베딩 벡터는 없음.StreamItems는 이를 제거하므로, 소비자, 트레이스 또는 기록에 도달하지 않습니다. StreamItems는 항목이 끝나는 위치를 결정합니다; 클라이언트는 절대 그렇게 하지 않습니다. 델타는 다른item_id를 전달하거나, 다른 종류(하나의 인덱스 아래에서 생각 단계가 텍스트, 이미지, 텍스트로 가는 Interactions는 세 개의 항목)이거나, 스스로 항목을 시작할 때 다음 항목에 속합니다: 이름을 전달하는tool_call.delta(모든 제공자는 호출의 첫 번째 델타에서 한 번 보내므로, Chat Completions 호출은 자체 ID가 필요 없음), 이미지 (inline_thinking, 또는image/*MIME 유형의inline_data, 오디오는 하나의 항목 청크로 스트리밍), 임베딩 벡터. 그렇지 않으면 지금 스트리밍 중인 항목을 계속합니다: ID 없이 델타가 그렇게 하며, 게이트웨이가 ID를 넣는 것과 관계없이 호출의 인수도 마찬가지입니다. 항목의 ID 아래에 단독으로 전송된 Fidelity는 항목의 것입니다, 그것을 전달하는 종류에 관계없이 (이미지 생각 후 Interactionsthought_signature). 해당 파일의 종류별 테이블 — 증가하는 필드, 첫 번째 델타의 헤더 필드, 청크가 결합되는 방식 —는 스트림에서 유일한 종류별 코드입니다: done 항목은 증가하는 필드가 모든 추가 델타의 조인으로 대체된 항목의 첫 번째 델타입니다
...
[번역을 위해 잘림]