이틀 전 저는 임시로 만든 HOME 환경에서 OpenCode의 opencode upgrade --major를 실행했고 실제로 어떤 세 가지 변경 사항이 작동하는지에 대해 글을 썼습니다. 서버 API는 제가 건너뛰어야 했던 부분이었습니다. 그 테스트에서는 클라이언트 통합 부분이 없었기 때문에, 추측하는 대신 정확히 그렇게 말했습니다.
여기는 후속 내용입니다: 동일한 @opencode/client를 같은 머신에서 v2 서버와 v1 서버에 각각 연결하여 5번씩 호출했고, 모든 것을 기록했습니다.
One client package, two servers: what each call did
설정 (The setup)
두 개의 서버가 있고, 둘 다 로컬에 있으며, 동일한 big-pickle 모델을 사용합니다:
# v2: 2.0.28 버전이며 부팅 시 서버 비밀번호를 출력함
HOME=/tmp/oc2test/home2 opencode2 serve --port 7911
# server listening on http://127.0.0.1:7911
...
하나의 클라이언트:
그리고 /tmp/oc2test/api/probe-v2-client.mjs라는 스크립트가 OpenCode.make({ baseUrl, headers }) → server.info → model.list → session.create → session.prompt → message.list를 폴링합니다. 같은 파일이지만 두 개의 TARGET_BASE 값을 사용했습니다.
각 호출이 실제로 수행한 작업 (What each call actually did)
server.info — v2는 `{
model.list — 흥미로운 부분입니다. v2의 경우: 12개의 모델을 반환하며, 객체는 { modelID, settings, package, compatibility } 형태를 가집니다. v1의 경우: 클라이언트가 0개 모델을 파싱하고 아무것도 발생시키지 않습니다. 동일한 v1 경로에 대해 원시 curl을 실행하면 39개의 모델이 반환됩니다. 구조는 { id, providerID, api, request }이므로, modelID를 순회하는 v2 클라이언트는 아무것도 찾지 못하고 조용히 빈 목록을 보고합니다. 침묵의 빈 결과는 예외보다 더 나쁩니다. 루프가 그냥 아무것도 하지 않기 때문입니다.
session.create — 두 버전 모두에서 변경 없이 통과한 유일한 호출입니다. v2는 40자리 16진수 projectID를 가진 ses_...를 반환하고, v1은 리터럴 문자열 `
v1.19.0 서버는 인증(auth) 기능이 없습니다. OPENCODE_SERVER_PASSWORD가 설정되지 않았다는 경고를 하나 기록할 뿐이며, 포트에 접근하는 누구에게나 서비스를 제공합니다. 마이그레이션 중이라면, 스크립트에서 가장 먼저 확인해야 할 부분이며, 필드 이름보다 더 큰 문제입니다. v1 서버가 루프백(loopback) 주소 외의 다른 곳에 바인딩된 것은 셸 명령어(shell commands)를 실행할 수 있는 에이전트에게 열린 문과 같습니다. (지난번에는 /global/upgrade 엔드포인트에서 원격 코드 실행(remote-code-execution) 취약점이 있었으며, Datadog에 의해 기록되고 2026년 8월에 패치되었습니다. 이는 인증되지 않은 제어 포트가 얼마나 가치가 있는지 상기시켜 줍니다.)
"v1 has /api/*"는 함정입니다
두 개의 OpenAPI 문서를 모두 추출하여 표면적(surfaces)을 비교했습니다:
v2.0.28: 118개 경로(/) / 142개 작업, 모두 /api/* 아래에 있음
v1.19.0: 162개 경로(/) / 188개 작업 — 레거시 /session/* 및 /api/*
표면적(surface)인데, 이의 작업 ID(operationIds)는 이미 "v2."로 접두사가 붙어 있습니다.
따라서 v1.19.0은 미리 시드된(/pre-seeded) /api 표면적을 가지고 출시되었습니다. 이것이 session.create가 v2 클라이언트에서 작동하는 이유이며, "경로는 존재하니 괜찮다"는 테스트가 잘못되었다는 이유이기도 합니다. 경로는 존재하지만, 요청 본문(request bodies)은 일치하지 않습니다. 공식 V1에서 마이그레이션하는 가이드에서는 서버 API와 클라이언트가 "새로운 계약(new contracts)"을 가지고 있다고 말하는데, 이 문장이 호출 지점(call site)에서 어떤 비용을 초래하는지 보여줍니다.
클라이언트 코드를 건드리기 전에 제가 실행할 체크
# 1. 서버가 실제로 존재하는지, 그리고 버전은 무엇인지?
curl -su ":${OPENCODE_PASSWORD}" http://127.0.0.1:7911/api/info
...
마이그레이션 브랜치에서 수행할 두 개의 명령어입니다. 이 두 가지 계약 형태(contract shapes)만 알면 나머지 포트는 기계적입니다.
결론
OpenCode 2는 통합을 위한 버전 업그레이드가 아니라, 친숙한 패키지 이름으로 사용되는 다른 백엔드입니다. 제가 수행한 다섯 번의 호출로 측정한 분할(split) 결과는 다음과 같습니다: 세 번은 잘못된 이유로 실패하거나 아무것도 반환하지 않았고, 한 번은 성공처럼 보이는 빈 리스트를 반환했으며, 단 한 번만 작동했습니다.
만약 타사 프로젝트가 이미 이 청구서를 지불했다면, 해당 프로젝트의 변경 로그(changelog)를 참고하는 것이 빠릅니다. 예를 들어, ai-sdk-provider-opencode-sdk 제공자는 @opencode/[email protected] 버전을 고정하고 이를 'SDK 업데이트가 아닌 새로운 백엔드'로 문서화하며, Basic-auth 요구 사항과 { data, error } 필드 스타일을 폐기하고 던져진 타입 오류(thrown typed errors)를 선호하는 것을 포함합니다. 이는 제가 반대 방향에서 겪은 것과 일치합니다. 동일한 클라이언트이지만 다른 서버이며, 매우 다른 동작 방식을 보입니다.
v2 런타임 자체는 좋은 의미로 지루했습니다. 세션(session), 프롬프트(prompt), 스트리밍 응답(streaming reply), 토큰(tokens), 비용(cost)이 모두 하나의 파일에서 한 번의 처리로 이루어졌습니다. 마이그레이션 과정의 어려움은 전적으로 두 계약(contract) 사이의 연결 부분에 있습니다.
위 내용은 2026-10-13 자 로컬 실행 결과입니다: /tmp/oc2test/api/에는 프로브 스크립트, 두 개의 실행 로그, 두 개의 OpenAPI 덤프, 그리고 원본 v1/v2 메시지가 보관되어 있습니다. 출처: 공식 V2 마이그레이션 및 플러그인 마이그레이션 가이드, Anomaly 2.0 노트, The New Stack의 2.0 릴리스 작성 글, Datadog Security Labs의 GHSA-632h-h47v-g4x4.