API 게이트웨이·앱
65e8040 기준입니다.
magpie는 여러 코딩 에이전트가 여러 AI 제공자의 모델을 한 주소로 이용하도록 묶는 데스크톱 앱 겸 로컬 게이트웨이입니다. 사용자는 화면에서 모델을 고르고, 에이전트는 자기에게 익숙한 API로 요청합니다. 그 사이에서 magpie가 설정 변경과 요청 중계를 맡습니다.
이 레포의 핵심은 에이전트가 말하는 API와 모델 제공자가 받는 API를 분리하는 데 있습니다. 예를 들어 Claude Code가 Anthropic Messages 형식으로 보낸 요청을, 다른 제공자가 받는 OpenAI Chat 형식으로 바꾸는 경로가 실제 소스에 있습니다.
에이전트는 전화기, API 형식은 통화 언어, 제공자 계정은 회선입니다. 교환국은 상대가 알아듣게 말을 바꾸고, 통화할 수 없는 회선 대신 다른 회선을 고릅니다.
화면의 모델 선택은 교환국의 안내 데스크에 해당합니다. 실제로 공부할 부분은 그 뒤의 번역 규칙·회선 선택·끊김 처리입니다.
| 사용자의 행동 | 내부 책임 | 읽을 소스 |
|---|---|---|
| 모델 선택 | 에이전트의 모델·연결 주소 등 설정을 변경 | internal/agent/, internal/edit/ |
| 에이전트에 질문 | HTTP 요청을 받고 제공자에게 전달하거나 형식 변환 | internal/gateway/gateway.go |
| 계정 한도 도달 | 다른 계정·키·대체 모델 후보를 시도 | internal/gateway/fallback.go |
| 기능 확장 | 내장 기능의 수명 관리 또는 외부 JS 플러그인 호출 | internal/pluginrt/, internal/plugin/ |
기본 게이트웨이 주소는 127.0.0.1:3425입니다.
internal/gateway/gateway.go의 DefaultAddr에서 확인할 수 있습니다.
127.0.0.1은 자기 컴퓨터를 가리키며, 기본 주소를 쓰면 다른 기기의 공개 서버로 열리는 구조는 아닙니다.
모델 계산 자체는 연결한 제공자나 별도 로컬 모델 서버가 담당합니다. 따라서 “로컬 게이트웨이”라는 말만으로 모든 프롬프트가 컴퓨터 안에 머문다고 이해하면 안 됩니다. 데이터가 전달되는 범위는 연결한 제공자와 플러그인의 처리 경로에 따라 달라집니다.
선정 근거는 TrendShift Monthly #10입니다. 2026-10-23 GitHub 기준으로 생성일은 2026-09-23이며, 스타 19,797개, 포크 1,502개입니다. 약 한 달 만의 규모라는 점은 확인할 수 있지만, 특정 기능이 별 증가를 일으켰다는 인과관계까지 증명하지는 못합니다.
아래 세 가지가 관심을 끌었을 이유라는 것은 README와 구현을 바탕으로 한 해석입니다. 각 요인의 기여도를 판단하려면 이용자 조사나 유입 통계가 필요합니다.
CC Switch·LiteLLM과 비교할 때는 설정 연결, API 변환, 라우팅을 어느 계층에서 다루는지 살펴보세요. 아래 표는 개인 개발 환경 통합과 소스 학습의 관점에서 살펴볼 항목을 보여 줍니다.
| 제품 | 공식 자료·비교할 지점 | magpie에서 살펴볼 구현 |
|---|---|---|
| CC Switch | 설정 전환·API 변환·폴백의 지원 범위와 실행 형태를 비교한다. magpie README는 CC Switch를 제공자 설정 가져오기 대상으로 명시한다. 공식 README | magpie는 같은 Go 본체에서 앱·CLI·TUI·웹 UI 경로를 다룬다. 화면 없는 실행과 데스크톱 실행이 어디서 갈리는지 한 레포로 배울 수 있다. |
| LiteLLM | API 통합·라우팅·비용 관리의 범위와 요청 처리 계층을 비교한다. 공식 개요 · 요청 처리 구조 | magpie는 로컬 에이전트의 설정 파일을 바꾸는 앞단까지 연결한다. 메뉴 막대의 선택과 실제 HTTP 변환을 이어 읽는 개인 개발 환경 사례가 된다. |
| magpie | 에이전트별 설정 연결, 네 종류 API의 변환 계층, 계정·키별 라우팅, 네이티브 앱과 터미널·웹 경로 — 분석 커밋의 소스에서 확인 | Go의 파일 편집·HTTP·프로세스 통신을 한 제품 안에서 비교할 수 있다. 이 구조만으로 상대 제품보다 빠르거나 안정적이라고 판단할 수는 없다. |
CC Switch에서 설정을 가져오는 기능은 magpie README에도 명시되어 있습니다.
서로 겹치는 영역이 있으므로 도구 이름만 보고 구분하기보다,
Server.translate(), Server.plan(), Field.Set처럼
실제 책임이 어느 계층에 있는지 비교해 보세요. 제품별 지원 범위는 각 공식 문서를 기준으로 살펴보면 됩니다.
초보자에게 이 레포가 흥미로운 이유는 화면의 클릭 하나를 파일 변경과 네트워크 동작까지 따라갈 수 있기 때문입니다.
internal/edit/에는 BOM·CRLF·심볼릭 링크·하드 링크 관련 테스트가 있고,
internal/gateway/에는 도구 호출 ID, 스트림 중 오류, 재시도, 계정 고정 관련 테스트가 있습니다.
“정상 요청 한 번” 다음에 무엇을 고민해야 하는지 배울 수 있습니다. 테스트 파일의 존재만으로 모든 환경에서의 통과를 보장할 수는 없습니다.
README는 여러 구독을 한곳에서 사용한다고 설명하며 Grok도 예시에 넣습니다.
하지만 internal/provider/migrate.go의 retiredBuiltins에는
grok, cursor, kiro, zed 등이 있습니다.
이 구독들의 내장 처리 코드는 폐기되었고, 해당 구독은 외부 플러그인이 담당합니다.
BuiltinRetired()와 PluginServes()를 보면,
내장 코드가 계속 처리하는 것이 아니라 플러그인 경로를 사용하도록 구분합니다.
따라서 “지원”이라는 README 문구는 모든 기능이 본체에 내장되어 있다는 뜻이 아닙니다.
플러그인의 설치·인증 상태와 변경에도 영향을 받습니다.
버전의 기준 파일은 루트 go.mod입니다.
언어 선언은 Go 1.26.8이며, 아래 버전은 최신 권장값이 아니라 분석 커밋에 적힌 값입니다.
| 기술 | 실제 버전 | 역할과 확인 지점 |
|---|---|---|
| Go 표준 라이브러리 | go 1.26.8 | net/http, encoding/json, context로 중계·취소·직렬화 |
| Wails v3 | v3.0.0-beta.24 | internal/gui/app.go의 네이티브 앱·트레이·창 |
| Bubble Tea | v1.3.10 | internal/tui/tui.go의 터미널 UI |
| Bubbles | v0.21.1 | TUI 입력 등 컴포넌트 |
| Lip Gloss | v1.1.1-0.20250404203927-76690c660834 | 터미널 스타일. 정식 태그처럼 줄여 쓰면 안 되는 의사 버전 |
| go-toml/v2 | v2.2.4 | TOML 구조 해석 |
| gjson · jsonc | v1.19.0 · v0.3.3 | internal/edit/json.go의 JSON 경로 조회·주석 포함 입력 처리 |
| yaml.v3 | v3.0.1 | YAML 설정 해석 |
| modernc.org/sqlite | v1.44.3 | 세션 데이터베이스 접근. 드라이버 진입점은 internal/sqlitedb/ |
| modernc.org/libc | v1.67.6 | sqlitedb.init()가 환경 변수 형식을 정리해 전달 |
| klauspost/compress | v1.18.3 | 압축 관련 의존성 |
| x/net · x/sys | v0.56.0 · v0.46.0 | 네트워크·운영체제 보조 기능 |
SQLite 의존성이 있다고 해서 모든 상태가 SQLite에 저장되지는 않습니다.
제공자·설정·마이그레이션·대화 고정에는 JSON 파일도 등장합니다.
migrations.json과 affinity.json은 각각 다른 책임의 상태입니다.
| 구성 | 확인 내용 | 초보자가 주목할 점 |
|---|---|---|
| 화면 | internal/gui/assets/의 HTML·CSS·JavaScript | internal/gui/assets/app.js는 뷰별 상태와 DOM 렌더링을 직접 관리한다 |
| 웹 프레임워크 | internal/gui/assets/app.js가 프레임워크 없는 구성을 명시 | React·Vue 앱으로 가정하고 읽지 않는다 |
| 정적 파일 배포 | internal/gui/api.go의 //go:embed assets | 화면 자산을 Go 실행 파일 안에 포함한다 |
| 외부 JS 플러그인 | internal/plugin/host.js, internal/plugin/pi.js | Go 프로세스와 별도 Bun 프로세스 사이의 메시지 교환 |
| Bun | BunVersion = "1.4.2" — internal/plugin/bun.go | 기본 다운로드 버전이자 최소 실행 기준. 더 새 버전도 허용한다 |
| 화면 녹화 도구 | Playwright 1.61.1 — .github/ui-preview/package.json | 앱 런타임 의존성과 PR 미리보기 도구를 구분한다 |
루트에는 앱용 package.json이 없습니다.
하위 테스트 fixture의 package.json을 앱의 전체 의존성으로 착각하지 않는 것이 중요합니다.
또한 내장 Go 플러그인의 수명 관리 계층 pluginrt와 외부 JS 실행 계층 plugin은 별개입니다.
| 영역 | 파일에 적힌 설정 | 해석 |
|---|---|---|
| 일반 앱 빌드 | Makefile: production, Linux는 gtk3 추가 | 운영체제의 웹뷰를 사용하는 데스크톱 앱 |
| CLI 빌드 | CGO_ENABLED=0, -tags nogui | 네이티브 창 의존성을 빼고 터미널·웹 UI 경로 유지 |
| 컨테이너 | golang:1.26-alpine → debian:12-slim | 다단계 빌드. Go 선언은 1.26.8이지만 이미지 태그는 1.26 계열 |
| 컨테이너 권한 | UID/GID 65532, VOLUME /config | 일반 사용자로 동작하고 설정·인증·캐시를 볼륨에 보관 |
| CI | .github/workflows/test.yml | Ubuntu·macOS·Windows 행렬, Go 버전은 go.mod 참조 |
| 검증 도구 | go vet, go test, 일부 -race, Bun 1.4.2 | Windows는 전체 테스트 컴파일과 선택된 네이티브 검사를 구분 |
| 배포 사이트 | site/worker.js, site/wrangler.jsonc | Cloudflare Worker 기반 다운로드·문서 사이트. 로컬 게이트웨이와 별도 |
그림은 핵심 흐름을 압축한 것입니다. 구독에 따라 로컬 CLI를 사용하는 별도 브리지도 있고, 본문에 없는 검색·도구·미들웨어 경로도 있습니다. 모든 요청이 무조건 같은 변환 단계를 거친다고 해석하면 안 됩니다.
시작점은 루트 main.go의 main()입니다.
본체는 cli.Main(version)에 위임하고,
plugins/all의 빈 식별자 import로 내장 플러그인 등록을 일으킵니다.
처음부터 거대한 게이트웨이 파일의 첫 줄부터 읽기보다 이 입구를 확인하는 편이 좋습니다.
internal/gateway/gateway.go의 Server.Handler()는 실제 URL과 처리 함수를 연결합니다.
다음은 소스에서 일부 경로만 발췌한 것입니다.
mux.HandleFunc("POST /v1/chat/completions", s.handle(provider.Chat))
mux.HandleFunc("POST /v1/responses", s.handle(provider.Responses))
mux.HandleFunc("POST /v1/messages", keepsSignIn(s.handle(provider.Anthropic)))
parse()는 parseChat(), parseResponses(),
parseGemini(), parseAnthropic()로 분기합니다.
그 결과는 internal/gateway/ir.go의 공통 Request입니다.
반대쪽 build()와 buildFor()는 제공자가 받는 형식으로 다시 만듭니다.
아래는 Request의 핵심 필드만 남긴 학습용 발췌입니다.
원본에는 캐시·형식·제공자별 옵션 등 더 많은 필드가 있습니다.
// internal/gateway/ir.go의 필드 일부
type Request struct {
Model string
System string
Messages []Message
Tools []Tool
ToolChoice string
MaxTokens int
Stream bool
}
Part는 텍스트·이미지·파일·도구 호출·도구 결과·추론 블록을 나누어 담습니다.
도구 호출에는 ID, Name, Args가 있고,
결과에는 CallID가 있습니다.
따라서 단순히 content 문자열만 복사하는 번역으로는 에이전트의 도구 왕복을 보존할 수 없습니다.
응답 쪽도 Event를 공통 언어로 씁니다.
KText는 본문 조각, KToolStart는 도구 시작,
KToolArgs는 아직 완성되지 않은 인자 조각입니다.
decoder()가 제공자별 이벤트를 읽고 encoder()가 호출자의 형식으로 내보냅니다.
Server.translate()는 호출자가 완성 응답을 원해도 제공자에게는 스트리밍을 요청합니다.
호출자가 스트리밍을 원하면 streamTranslated()로 가고,
아니면 이벤트를 수집해 완성 응답을 만듭니다.
제공자가 stream:true를 무시한 경우를 감지하는 오류 경로도 있습니다.
설계의 대가: 공통 자료형을 만든다고 API 차이가 사라지지는 않습니다.
Part.Sealed와 SealedBy는 특정 API가 봉인한 추론 데이터를 구분합니다.
현재 사용자 메시지의 이미지가 변환할 수 없는 형태라면 translate()에서 400으로 거절하는 경우도 있습니다.
“형식 호환”과 “기능을 모두 동일하게 재현”은 구별해야 합니다.
internal/routes/routes.go의 Strategy는 후보 순서를 반환합니다.
Seat는 사용률·갱신 시각·가중치·휴식 여부 등 순서를 정할 자료를 담습니다.
반면 후보를 만드는 일과 실제 재시도는 게이트웨이에 남겨 둡니다.
// internal/routes/routes.go의 인터페이스 핵심
Order func(pool string, seats []Seat) []int
// 예: 세 후보의 우선순위가 두 번째, 첫 번째, 세 번째라면
[]int{1, 0, 2}
Register()는 전략을 등록하고 해제 함수를 돌려줍니다.
plugins/rotation/rotation.go는 등록 결과를 c.OnDispose()에 연결합니다.
기능이 꺼질 때 등록도 해제되도록 생명주기를 묶는 방식입니다.
Apply()는 반환값이 모든 인덱스를 한 번씩 포함하는 순열인지 검사합니다.
중복·누락·범위 초과 또는 panic이 있으면 원래 순서로 복구합니다.
이것은 인증을 생략한다는 뜻이 아니라, 정렬 기능의 실패 때문에 후보 자체를 잃지 않도록 한다는 뜻입니다.
| 소스의 경계 | 담당 함수·자료 | 학습할 질문 |
|---|---|---|
| 후보 만들기 | internal/gateway/fallback.go의 perKeyBarred(), plan(), planGroup() | 한 모델에 키가 둘이면 시도 대상은 몇 개인가? |
| 순서 고르기 | routes.Smart, Ordered, Pace 등 | 정책이 요청 실행 코드를 직접 알아야 하는가? |
| 실패 후 휴식 | fallbackCooldown = time.Minute | 막 실패한 후보를 다음 요청에서도 먼저 부를 것인가? |
| 대화 고정 | internal/gateway/affinity.go의 affine(), answered() | 균등 분배가 프롬프트 캐시를 깨뜨리지 않는가? |
internal/routes/strategies.go의 Smart는 단순한 무작위 분배가 아닙니다.
사용률 구간을 나누고, 여유 있는 계정 중 큰 한도 창의 갱신 시각을 우선합니다.
갱신 시각 비교는 시간 단위로 묶으며, LowShare는 90입니다.
잔량을 아직 모르지만 응답에서 배울 수 있는 계정을 먼저 탐색하는 규칙도 있습니다.
곧 만료되는 식권을 먼저 쓰는 것이 Smart에 가깝습니다. 다만 같은 손님을 계속 다른 창구로 보내면 이전 작업을 다시 설명해야 합니다. 그래서 대화 고정은 “전에 응대한 창구를 유지할 이익”도 함께 고려합니다.
여기서 요청 한 번과 모델 제공자 호출 시도 한 번은 다릅니다.
후보가 실패하면 하나의 요청 안에서도 여러 시도가 기록될 수 있습니다.
internal/gateway/fallback.go는 이미 사용자에게 보낸 응답을 두 모델의 반쪽 답변으로 섞지 않도록 경계를 둡니다.
1분 휴식은 일반 실패의 기본값이며, 모든 한도·오류를 무조건 1분으로 처리한다는 뜻은 아닙니다.
재시도의 핵심은 몇 번 더 부르느냐보다 어느 시점까지 답변을 바꿔도 되는지 정하는 것입니다.
internal/gui/api.go의 POST /api/set 처리기는
agent.Find() → a.Field() → a.Pick() 순서로 선택을 넘깁니다.
internal/agent/agent.go의 Field에는
Get, Set, Options 함수가 있습니다.
화면은 각 에이전트의 파일 문법을 직접 알 필요가 없습니다.
internal/agent/codex.go는 Codex의 TOML 키를 연결하고,
internal/edit/json.go의 SetJSON()은 필요한 키를 수정한 뒤 저장합니다.
객체 전체를 다시 직렬화해 파일을 통째로 정렬하는 것보다 주석·들여쓰기·키 순서를 보존하는 데 초점을 둡니다.
internal/edit/file.go의 WriteAtomic()은
대상 경로 확인, BOM 보존, 파일 권한 보존, 같은 디렉터리의 임시 파일 생성,
steady.Sync(), steady.Rename() 순서로 일반 파일을 교체합니다.
JSON 편집의 keepCRLF()는 원본이 일관된 CRLF일 때 그 줄바꿈을 유지합니다.
중요한 예외도 소스에 있습니다.
심볼릭 링크는 링크가 가리키는 파일을 수정합니다.
하드 링크는 다른 이름과의 연결을 유지하려고 writeInPlace()로 덮어쓰는 경로가 있으며,
이 경로는 원자적이지 않습니다. 대신 새 내용을 동기화한 임시 파일을 먼저 남깁니다.
internal/plugin/host.go의 startOn()은 Bun으로 호스트를 시작하고
StdinPipe(), StdoutPipe()를 연결합니다.
Go와 JS는 요청 ID가 있는 줄 단위 JSON 메시지를 주고받습니다.
바이너리 응답 조각은 메시지의 Data와 디코딩된 바이트로 관리됩니다.
독립 프로세스라고 자동으로 신뢰할 수 있는 샌드박스가 되지는 않습니다.
여기서 주목할 것은 느린 소비자 때문에 전체 플러그인 통신이 막히지 않게 하는 설계입니다.
각 호출은 큐와 상태를 가지며, call.push()가 받은 조각을 관리합니다.
// internal/plugin/host.go의 기본값
streamWindow = 512 << 10 // 호출별 크레딧: 512 KiB
streamBudget = 64 << 20 // 스트림 윈도 전체 예산: 64 MiB
maxFrame = 64 << 10 // 조각 크기 상한: 64 KiB
frameOverhead = 2048 // 조각 메타데이터도 비용에 포함
자식 프로세스는 받은 크레딧 범위에서 조각을 보내고, 소비자가 읽으면 여유를 돌려받습니다.
크레딧을 초과한 응답은 프로토콜 위반으로 처리합니다.
maxWireLine은 끝없이 길어지는 메시지를 막고, 헤더 수·크기에도 별도 상한이 있습니다.
internal/plugin/host.js의 pump()는 출력 큐를 비우며,
표준 출력의 write()가 가득 찼다고 알리면 waitDrain()으로 기다립니다.
false가 “그 줄을 못 썼다”는 뜻은 아니므로 같은 줄을 다시 보내지 않는다는 점도 주석에 드러납니다.
플러그인이 물을 보내는 수도꼭지라면, 호출별 큐는 작은 물통입니다. 물통이 가득 찼는데 계속 붓지 않도록 남은 공간을 알려 주는 것이 역압입니다. 64 MiB는 이 통신 장치의 예산이지 앱 전체 RAM 상한이 아닙니다.
고정 소스 27bf14790f08b1fd12a354fbdebedf692420894b의 README와 internal/provider/discovery.go는 구독 계정 탐색을 별도 선택으로 둡니다. 기본 동작에서는 에이전트의 기존 로그인 정보를 찾지만, Settings → Privacy의 Subscription accounts를 끄거나 아래 설정을 쓰면 자격증명 파일·Keychain과 구독 계정을 읽는 경로를 차단합니다.
# 프로젝트 문서의 설정 예시. 이번 분석에서 실행하지 않았습니다.
magpie accounts discovery off
# 한 실행에서만 끄는 환경 변수
MAGPIE_NO_ACCOUNTS=1
settings.AccountDiscoveryOffIn은 저장된 NoAccountDiscovery와 환경 변수를 함께 확인하고, readAccountFile은 탐색이 꺼졌으면 파일을 읽기 전에 오류를 반환합니다. API 키 제공자는 계속 쓸 수 있고, 이미 저장된 magpie 자체 로그인 자료는 삭제하지 않고 읽기·쓰기를 중지합니다. 저장된 자료 삭제와 탐색 중지는 다른 동작입니다.
실습은 settings/accounts.go → provider/discovery.go를 읽고, 자격증명 파일을 읽기 전 검사와 UI 설정의 관계를 그려 보는 것으로 시작합니다. 에이전트 설정·구독 계정·외부 모델을 실제로 실행한 분석은 아닙니다.
근거: 탐색 설정 판정 · 파일 읽기 경계. 아래 초판의 세부 프로토콜 예시는 당시 커밋 65e8040 기준으로 남깁니다.
이 트리는 학습에 필요한 항목만 추린 것입니다.
Go는 internal/의 패키지를 그 부모 디렉터리 트리 안에서만 import하도록 제한합니다.
이 레포의 대부분은 외부 공개 라이브러리보다 앱 내부의 책임 단위로 나뉩니다.
저장소 루트(.) 아래 파일의 LF 바이트 수를 기준으로 한 규모입니다.
.git/은 제외했고, 테스트·fixture·생성 코드·사이트는 포함했습니다.
따라서 주석·빈 줄을 뺀 순수 로직 줄 수나 GitHub 언어 비율과는 다릅니다.
| 언어·확장자 | 파일 수 | 줄 수 |
|---|---|---|
Go · .go | 3,480 | 708,645 |
| 그중 Go 테스트 | 2,420 | 390,800 — 위 Go 수치에 포함 |
JavaScript · .js | 50 | 75,138 |
JavaScript · .cjs | 847 | 126,736 |
JavaScript · .mjs | 5 | 1,883 |
| HTML | 31 | 10,475 |
| CSS | 11 | 7,086 |
| TypeScript | 1 | 221 |
| Shell | 12 | 626 |
Objective-C · .m | 2 | 182 |
C 계열 헤더 · .h | 2 | 306 |
Go에서 _test.go를 제외하면 317,845줄입니다.
Go 테스트는 약 55.1%이며, 이 비율은 코드 커버리지나 품질 점수가 아닙니다.
파일 개수와 줄 수만으로 학습 부담이나 안정성을 판단하지 않는 것이 좋습니다.
# 저장소 루트에서 같은 기준으로 세는 읽기 전용 명령
python3 - <<'PY'
from pathlib import Path
files = [p for p in Path('.').rglob('*.go') if '.git' not in p.parts]
print(len(files), sum(p.read_bytes().count(b'\n') for p in files))
PY
internal/gateway/gateway.go 하나도 7,065줄입니다.
처음에는 internal/gateway/ir.go → Handler() → parse() →
translate() 순서로 읽고, 그다음 internal/gateway/fallback.go로 이동하세요.
이름을 검색해 작은 범위씩 확인하는 습관이 큰 레포를 읽는 기술입니다.
Server.Handler()에서 http.NewServeMux()와
메서드가 포함된 패턴을 읽습니다.
s.handle(provider.Chat)처럼 프로토콜을 전달해 처리 함수를 만드는 방식은
비슷한 엔드포인트의 공통 동작을 묶는 사례입니다.
작은 실습: POST /v1/responses를 찾아
handle() → serve() → attempt()로 이어지는 호출 지도를 적습니다.
그다음 translate()가 필요한 경우와 그대로 전달할 수 있는 경우를 구분합니다.
internal/gateway/ir.go의 Message, Part, Event를 보면서
사용자 본문, 추론, 도구 호출, 도구 결과가 왜 다른 종류인지 생각합니다.
특히 도구 호출 ID와 결과의 CallID가 맞아야 대화의 관계가 보존됩니다.
작은 실습: “파일 읽기 도구를 요청 → 도구가 내용을 반환 → 모델이 요약”을 텍스트 세 줄이 아닌 구조화된 표로 그려 보세요. 각 단계의 역할·호출 ID·내용 종류를 열로 두면 무엇을 잃으면 안 되는지 보입니다.
internal/gateway/gateway.go의 decoder()·encoder()와
internal/plugin/host.go의 큐·잠금·취소 처리를 연결해 읽습니다.
하나의 도구 인자 JSON이 여러 이벤트에 나뉘어 올 수 있고,
소비자가 느리거나 연결을 끊는 경우도 따로 다뤄야 합니다.
작은 실습: 본문 이벤트, 오류 이벤트, 사용량 이벤트를 각각 하나씩 종이에 적고 “이미 화면에 표시한 내용”과 “아직 내부에만 있는 내용”을 다른 칸에 놓습니다. 어느 칸까지는 다른 모델로 재시도할 수 있는지 설명해 보세요.
edit.SetJSON(), keepCRLF(), WriteAtomic()를 읽으면
데이터 값이 맞는 것만으로는 충분하지 않다는 사실이 보입니다.
사람은 주석·줄바꿈·권한·링크 관계도 설정의 일부로 사용합니다.
작은 실습: internal/edit/crlf_json_test.go,
internal/edit/bom_test.go, internal/edit/hardlink_test.go에서
입력·기대한 결과·실패 조건을 하나씩 뽑습니다.
운영체제가 달라질 때 같은 “저장”에 어떤 추가 조건이 붙는지 정리합니다.
internal/gui/assets/app.js에는 state, prefs,
providers, pick처럼 목적이 다른 상태가 있습니다.
화면 상태가 서버의 영속 데이터와 같은 것은 아닙니다.
서버의 POST /api/set까지 함께 읽어야 실제 변경 경계가 보입니다.
작은 실습: 모델 선택 창이 열리는 순간부터 서버 응답을 받아 화면이 바뀌는 순간까지 “사용자 입력 → 임시 선택 → 저장 요청 → 서버 상태” 네 칸으로 나눕니다. 저장 실패 시 어떤 칸을 복원해야 할지도 적습니다.
이 프로젝트에서 배울 것은 특정 라이브러리의 문법보다 입력·상태·부작용의 경계를 나누는 방법입니다.
internal/routes/routes_test.go의 TestApplyFailsOpen()은
panic, 짧은 배열, 중복, 범위 초과, 음수, nil을 다룹니다.
각 예외를 암기하기보다 “정렬 전략이 망가져도 유효한 후보를 잃지 않는다”는 공통 조건을 찾으세요.
CI도 플랫폼마다 같은 검사를 한다고 단정하면 안 됩니다.
.github/workflows/test.yml은 Windows에서 전체 테스트를 컴파일하고 일부 네이티브 검사를 따로 실행합니다.
선언된 행렬과 실제 실행 단계를 구분해서 읽는 것 역시 학습 포인트입니다.
원격 API를 중계하는 magpie 자체와 로컬에서 모델을 계산하는 서버는 서로 다른 자원을 요구합니다. 소스의 설정값만으로 처리량, 실제 메모리 사용량, 권장 CPU 코어 수를 산정할 수는 없습니다.
| 사용 방식 | 소스로 확인한 조건 | 추가로 확인할 것 |
|---|---|---|
| macOS 앱 | build/darwin/Info.plist의 최소 버전 12.0, Makefile도 12.0 지정 | 배포 대상은 arm64·amd64. 실제 선택한 릴리스의 호환성 |
| Windows 앱 | Makefile의 amd64·arm64 빌드, 참조 문서 기준 WebView2 런타임 사용 | 해당 컴퓨터의 WebView2 설치 상태. 앱의 최소 RAM은 별도 검토 필요 |
| Linux 앱 | GTK 3·WebKitGTK 4.1, 앱 빌드에 CGO 사용 | 배포판 라이브러리·화면 세션. 빌드 시 개발 헤더 필요 |
| CLI·웹 UI | nogui 및 CGO_ENABLED=0 경로 | 네이티브 창 없이 사용할 운영체제·아키텍처 |
| Termux | Makefile의 Android 분기와 nogui; android/arm64 배포 대상 | 데스크톱 웹뷰 대신 웹 UI 또는 TUI 사용 |
| Docker | Debian 12 기반, 설정 볼륨 /config, 포트 3425·3430 선언 | 볼륨의 저장 공간·권한, 실제 호스트 포트 매핑 |
| 소스 빌드 | go.mod의 Go 1.26.8 | 앱 빌드는 플랫폼별 도구·웹뷰 의존성도 필요 |
| 외부 플러그인 | Bun 최소 기준 1.4.2, 지원 OS·아키텍처별 다운로드 분기 | 플러그인 추가 의존성·인증·네트워크 접근 |
| 로컬 모델 연결 | README가 Ollama·LM Studio 등을 제공자 예시로 듦 | GPU·VRAM·RAM은 별도 모델 서버와 모델 크기 기준으로 산정 |
streamBudget = 64 << 20은 플러그인 호스트의 스트림 윈도 예산입니다. 프로세스 전체가 RAM 64 MiB로 동작한다는 뜻이 아닙니다.EXPOSE 3425 3430은 이미지의 포트 선언입니다. 그 자체로 호스트에 포트를 공개하는 명령이 아닙니다.CGO_ENABLED=0은 CLI 빌드의 C 연결을 끕니다. Bun·외부 CLI·네트워크 같은 선택 기능의 요구까지 없애지는 않습니다.자원 계획을 한다면 먼저 원격 API 중계만 쓰는지, 플러그인과 구독 CLI도 쓰는지, 로컬 추론 서버도 같은 컴퓨터에서 돌리는지를 나누어야 합니다. 이 글의 수치는 앱 전체의 최소 RAM·디스크 요구량을 뜻하지 않습니다.
아래 과제는 읽기와 문서 작성만으로 수행할 수 있는 소스 독해 실습입니다.
설치나 실제 모델 호출 없이 코드와 테스트의 조건을 따라가 보세요.
설계 설명은 docs/subsystems/에서, 구현과 테스트는 internal/에서 찾아볼 수 있습니다.
목표: URL·프로토콜·처리 함수의 관계를 설명합니다.
internal/gateway/gateway.go에서 Server.Handler()를 찾습니다./v1/chat/completions, /v1/responses, /v1/messages를 표로 적습니다.provider.Protocol 값과 감싸는 함수를 비교합니다.제출물: 엔드포인트 → 프로토콜 → 처리 함수의 세 행 표.
완료 기준: 같은 모델을 불러도 요청 형식이 다를 수 있다는 사실을 자기 말로 설명할 수 있습니다.
목표: UI 이벤트와 파일 저장 사이의 책임을 나눕니다.
internal/gui/api.go에서 POST /api/set을 읽습니다.internal/agent/agent.go의 Field와 internal/agent/codex.go의 TOML 편집 호출을 연결합니다.internal/edit/file.go에서 일반 파일·심볼릭 링크·하드 링크의 저장 차이를 적습니다.제출물: 여섯 단계 이하의 호출 그림과 “하드 링크에서 원자성을 양보하는 이유” 한 문단.
완료 기준: JSON·TOML을 화면 코드가 직접 수정하지 않는 이유를 설명합니다.
목표: 구현보다 먼저 불변 조건을 표현합니다.
후보가 세 개일 때 아래 배열이 유효한지 판정하고,
routes.Apply()가 최종적으로 무엇을 돌려줄지 적습니다.
[]int{2, 0, 1}
[]int{2, 2, 0}
[]int{0, 1}
[]int{-1, 0, 1}
제출물: 입력·유효성·최종 순서·근거를 적은 네 행 표.
완료 기준: 첫 행은 유지되고 나머지는 []int{0, 1, 2}로 복구됨을
TestApplyFailsOpen()의 사례와 연결해 설명합니다. 테스트는 읽기만 합니다.
목표: 도구 호출과 도구 결과가 같은 ID로 연결되어야 함을 이해합니다.
internal/gateway/ir.go의 Part, Message, Event를 읽습니다.read_file, 호출 ID call_1, 인자 {"path":"README.md"}를 종이에 표시합니다.internal/gateway/anthropic.go와 internal/gateway/chat.go에서 호출·결과를 만드는 부분을 찾아 대응시킵니다.제출물: 원본 API 개념 → IR 필드 → 대상 API 개념의 대응표.
완료 기준: 결과의 CallID를 잃으면 무엇이 연결되지 않는지 설명합니다.
인자 조각이 아직 완전한 JSON이 아닐 수 있다는 점도 적습니다.
목표: 논리적 요청 수와 실제 제공자 호출 수를 구분합니다.
internal/gateway/plugin_workdir_test.go의
TestPluginToldWhereTheAgentWorks()를 읽습니다.
에이전트의 실행 디렉터리 정보가 플러그인에 전달되고 관련 내부 헤더가 외부 제공자에게 전달되지 않는지 검사합니다.
got을 초기화하는 이유를 적습니다.len(sent) == 0 || len(sent) > len(rt.Tries)를 실패 조건으로 둔 이유를 적습니다.sawWorkPlace()가 확인하는 헤더의 목적과 전달 범위를 정리합니다.제출물: 시도 A 실패 → 시도 B 성공의 시간선과, 잡아야 할 중복 호출의 예.
완료 기준: 무조건 외부 요청 1회를 기대하면 정상 재시도를 오탐할 수 있지만, 성공 뒤 재시도나 기록 없는 추가 요청을 허용해서도 안 되는 이유를 설명합니다.
좋은 독해 실습은 함수를 찾는 데서 끝나지 않고 정상 동작과 버그를 가르는 조건을 결과물로 남깁니다.
아래 시간은 공식 교육 과정이나 숙련도 보장이 아닌 학습 계획 예시입니다. 매주 읽을 범위를 좁히고, 설명 자료 한 장을 만드는 것을 목표로 합니다.
| 주차 | 기초 개념 | 소스 읽기 | 남길 결과물 |
|---|---|---|---|
| 1주 | Go 모듈·함수·구조체·HTTP 경로 | main.go, internal/cli/main.go, Server.Handler() | 프로그램 입구와 세 엔드포인트 지도 |
| 2주 | JSON·프로토콜·어댑터·도구 호출 ID | internal/gateway/ir.go, parse(), build() | API 두 종류의 필드 대응표 |
| 3주 | 이벤트·SSE·부분 데이터·취소 | decoder(), encoder(), translate() | 본문·도구 인자·오류 이벤트 시간선 |
| 4주 | 전략 패턴·정렬·재시도·캐시 | internal/routes/, internal/gateway/fallback.go, internal/gateway/affinity.go | 후보 세 개의 성공·실패·고정 시나리오 |
| 5주 | 파일 권한·줄바꿈·링크·디스크 동기화 | internal/agent/agent.go, internal/edit/json.go, internal/edit/file.go | 일반 파일과 하드 링크의 저장 비교 |
| 6주 | 프로세스 간 통신·역압·기능 수명 | internal/plugin/host.go, internal/plugin/host.js, plugins/rotation/ | 호출 시작·소비·취소·해제 상태 그림 |
Go가 처음이라면 1주차 전에 struct, 슬라이스, 함수 값,
포인터, error, defer를 먼저 익히세요.
goroutine과 채널을 모른 채 플러그인 호스트부터 읽으면 통신 규약보다 문법에 막히기 쉽습니다.
각 주의 마무리는 코드 요약 대신 “이 경계가 없다면 어떤 문제가 생길까?”를 쓰는 것입니다. 예를 들어 IR이 없으면 변환 규칙이 중복되고, 응답 공개 경계가 없으면 서로 다른 모델의 답이 섞일 수 있습니다.
한 번에 레포 전체를 이해하려 하지 말고 한 요청의 정상 경로를 그린 뒤 실패 경로를 하나씩 붙이세요.
docs/subsystems/README.md를 목차로 사용하세요.
라우팅은 docs/subsystems/gateway-routing.md, 설정 연결은 docs/subsystems/agent-wiring.md,
UI는 docs/subsystems/gui-shell.md로 갈라집니다.
문서의 설명을 함수·상태·테스트에 대응시키면 단순한 코드 따라 읽기를 넘어설 수 있습니다.
| 용어 | 쉬운 설명 |
|---|---|
| 에이전트 | 모델에 질문하고, 도구를 실행하고, 그 결과를 다시 모델에 전달하는 프로그램. 여기서는 Codex·Claude Code 등이 해당한다. |
| 제공자 · provider | 모델을 호출할 연결 대상과 설정. 모델 이름뿐 아니라 주소·프로토콜·키·계정 같은 정보도 필요하다. |
| 게이트웨이 | 여러 요청이 통과하는 관문. 이 레포에서는 API 형식 변환과 후보 선택도 맡는다. |
| 루프백 · loopback | 네트워크를 통해 자기 컴퓨터로 돌아오는 연결. 기본 주소의 127.0.0.1이 해당한다. |
| 엔드포인트 | 특정 기능을 요청하는 API 주소. HTTP 메서드와 경로를 함께 보면 역할이 분명해진다. |
| 프로토콜 | 메시지를 어떤 필드와 순서로 주고받는지 정한 약속. 같은 모델이라도 API 약속이 다를 수 있다. |
| 어댑터 패턴 | 서로 맞지 않는 입출력 형태 사이에 변환기를 두는 설계. 여기서는 API별 파서와 빌더가 예다. |
| IR · 중간 표현 | 여러 외부 형식을 공통 내부 구조로 옮긴 것. 이 레포의 Request와 Event를 떠올리면 된다. |
| 직렬화 · 파싱 | 구조화된 값을 JSON 같은 전송 형식으로 쓰는 일이 직렬화, 전송된 내용을 자료형으로 읽는 일이 파싱이다. |
| 도구 호출 | 모델이 특정 함수와 인자로 작업을 요청하는 것. 호출 ID와 결과 ID를 맞춰야 한다. |
| SSE | HTTP 응답을 열린 채로 두고 이벤트를 차례로 보내는 방식. 조각 하나가 답변 전체인 것은 아니다. |
| 스트리밍 | 답변이 완성되기 전에 부분 결과를 보내는 방식. 오류와 완료 시점을 따로 구별해야 한다. |
| 전략 패턴 | 같은 작업의 정책을 바꿔 끼우는 설계. 이 레포에서는 후보 순서 계산을 Strategy로 교체한다. |
| 순열 · permutation | 모든 항목을 중복·누락 없이 다시 배열한 것. 세 후보라면 [2, 0, 1]은 유효하다. |
| 폴백 · fallback | 현재 후보가 처리할 수 없을 때 다음 계정·키·모델로 넘어가는 대체 경로. |
| 재시도 · retry | 실패한 작업을 다시 시도하는 것. 한 사용자 요청에 모델 제공자 호출이 여러 번 생길 수 있다. |
| 쿨다운 · cooldown | 막 실패한 후보를 잠시 쉬게 해 같은 실패를 반복하는 비용을 줄이는 장치. |
| 할당량 · quota | 계정이 일정한 시간 창 안에 사용할 수 있는 양. 사용량과 한도가 갱신되는 시각도 라우팅에 쓰인다. |
| 대화 고정 · affinity | 이전 답변을 준 계정·키에 대화를 유지하는 성질. 프롬프트 캐시와 도구 왕복의 연속성에 영향을 준다. |
| 프롬프트 캐시 | 제공자가 이미 처리한 입력 일부를 재사용하는 장치. 실제 절감 정도는 제공자 정책과 호출 조건에 따라 다르다. |
| 역압 · backpressure | 소비자가 처리할 수 있는 속도와 공간에 맞춰 생산자가 보내는 양을 제한하는 흐름 제어. |
| RPC | 다른 프로세스에 작업을 부탁하고 결과를 받는 방식. 여기서는 Go와 JS 호스트 사이의 ID가 있는 메시지를 떠올리면 된다. |
| 표준 입출력 | 프로세스가 기본적으로 읽고 쓰는 통로. 터미널 글자 출력뿐 아니라 프로세스 사이 통신에도 쓴다. |
| 생명주기 · dispose | 기능의 시작부터 해제까지 관리하는 과정. 기능을 끌 때 등록한 전략·자원도 정리해야 한다. |
| 원자적 교체 | 저장 도중의 반쪽 파일 대신 이전 파일 또는 새 파일이 보이게 교체하는 방식. 이 레포에는 하드 링크 예외도 있다. |
| BOM · CRLF | BOM은 파일 앞의 인코딩 표식, CRLF는 줄바꿈 방식이다. 값을 맞게 저장해도 둘을 깨뜨리면 도구 간 문제가 생길 수 있다. |
| 심볼릭 링크 · 하드 링크 | 다른 경로를 가리키는 링크와, 같은 파일 내용을 여러 이름으로 공유하는 링크. 저장 방식에 따라 관계가 바뀔 수 있다. |
| 빌드 태그 · CGO | 태그는 포함할 Go 파일을 고르는 조건, CGO는 C 코드 연결 기능. nogui는 네이티브 창을 빼는 경로다. |
| go:embed | 정적 파일을 Go 실행 파일에 포함하는 지시문. 이 레포는 UI 자산과 플러그인 호스트 코드 등에 사용한다. |
| 회귀 테스트 | 예전에 고친 문제가 다시 생기지 않게 같은 조건을 확인하는 검사. 테스트 파일을 읽으면 실제 예외 사례를 배울 수 있다. |
| race detector | 여러 실행 흐름이 같은 메모리를 잘못 공유하는 데이터 경쟁을 실행 중 찾는 Go 검사 기능. 모든 동시성 버그를 증명해 주는 것은 아니다. |
아래 링크는 개념을 더 공부할 출발점입니다. 이 글의 버전·설정값은 커밋 65e8040을 기준으로 합니다.
| 항목 | 확인 내용·근거 |
|---|---|
| 선정 | TrendShift Monthly #10 — 2026-10-23 기준 |
| 스타 / 포크 | 19,797 / 1,502 — 2026-10-23 GitHub 기준 |
| 주 언어 | Go — 2026-10-23 GitHub 기준 |
| 생성일 | 2026-09-23 — 2026-10-23 GitHub 기준 |
| 최근 push | 2026-10-23 — 2026-10-23 GitHub 기준 |
| 최신 릴리스 | v0.1.1330, 2026-10-23 — 2026-10-23 GitHub 기준. README의 릴리스 연결은 yetone/magpie-releases |
| 기준 커밋 | 65e8040 — 작성 시각 2026-10-23 14:42:53 +08:00 |
| 커밋 내용 | TestPluginToldWhereTheAgentWorks가 한 요청을 전제하지 않고 사례별 시도 횟수를 기준으로 검사하도록 수정 — #3347 |
| Go 코드 규모 | 708,645줄, 3,480파일. 테스트 390,800줄 포함 — LF 바이트 수 기준 |
| 그 밖의 코드 | JavaScript 합계 203,757줄, HTML 10,475줄, CSS 7,086줄 — 테스트·fixture·사이트 포함 |
| 라이선스 | MIT — 2026-10-23 GitHub 기준 및 루트 LICENSE |
| 소스 기준 | 커밋 65e8040의 소스·설정·문서·테스트. 장기간의 변경 이력과는 구분 |
| 동작 설명의 범위 | 구현된 처리 경로와 실패 조건. 실제 호환성과 처리 성능은 사용 환경에 따라 달라짐 |
① 호환은 무손실 변환의 보장이 아닙니다. API마다 도구·이미지·추론·서명·캐시 표현이 다릅니다. 실제 소스에도 전용 처리와 변환 거절 조건이 있습니다. 특정 에이전트와 모델의 모든 기능이 같다고 단정할 수 없습니다.
② 구독 지원의 담당이 이동했습니다.
Grok·Cursor·Kiro 등은 retiredBuiltins에 있으며 플러그인 경로에 의존합니다.
README의 지원 목록만으로 현재 인증·실행 경로를 판단하지 마세요.
구독을 다른 도구에서 사용하는 조건은 해당 제공자와 플러그인의 안내도 확인해야 합니다.
③ 로컬 앱과 외부 통신을 구분해야 합니다.
원격 모델을 연결하면 프롬프트가 그 제공자로 전달됩니다.
README는 기존 에이전트 인증 정보의 자동 탐색을 설명하며,
MAGPIE_NO_ACCOUNTS=1로 한 번의 실행에서 끌 수 있다고 안내합니다.
④ 사용 통계 기능도 있습니다.
internal/stats/stats.go의 Run()은 릴리스 빌드의 일별 통계를 다룹니다.
임의의 설치 ID·버전·운영체제를 보내며, 세부 통계가 켜져 있으면 에이전트·제공자·모델 식별자와 제휴사 노출·선택 횟수도 전송합니다.
이름·키·프롬프트·토큰 사용량은 제외합니다. 설정 또는 DO_NOT_TRACK=1, MAGPIE_NO_STATS=1로 끌 수 있습니다.
릴리스 여부는 버전 문자열로 판정하며, 통계 호스트를 따로 지정하면 이 조건을 건너뜁니다.
⑤ 이 글은 성능·안정성 보증이 아닙니다. 특정 커밋의 구현만으로 모든 에이전트·모델 조합의 성능과 안정성을 보장할 수는 없습니다. 테스트가 많다는 사실은 모든 환경에서 통과했다는 뜻이 아닙니다. 릴리스 번호·스타·지원 모델은 이후 달라질 수 있으므로 도입 시점에는 다시 확인해야 합니다.