Raycast 업데이트 후 기존 중계 프로젝트가 모두 작동하지 않았다. 새 요청 서명을 역분석해 raycast2api를 만들었다. Cloudflare Workers와 Docker에 배포하거나 독립 바이너리로 실행할 수 있다.
Raycast에는 여러 모델을 쓸 수 있는 AI가 내장되어 있다. 이를 OpenAI 호환 API로 바꿔 다른 클라이언트에서도 바로 쓸 수 있게 해 주는 유용한 프로젝트가 두 개 있다. 하나는 raycast-relay, 다른 하나는 missuo의 raycast2api다. 나도 그동안 둘 다 사용했다.
그런데 Raycast 업데이트 후 두 프로젝트 모두 작동하지 않게 됐다. 새 요청에 서명이 추가됐고, 이 서명 없이는 요청이 통과하지 못하기 때문이었다. raycast-relay는 이미 보관 처리되어 더 이상 유지보수되지 않았다. 그래서 직접 새 서명을 역분석하고 다시 만들었다. 이름은 raycast2api를 그대로 썼고, 저장소는 xxxbrian/raycast2api에 있다.
하는 일
프로젝트가 하는 일은 사실 하나뿐이다. OpenAI 형식의 요청을 Raycast 요청으로 바꾸고, Raycast 응답을 다시 되돌린다. 외부에 제공하는 엔드포인트도 몇 개 없다.
엔드포인트
용도
GET /v1/models
모델 목록
POST /v1/chat/completions
대화, 스트리밍·비스트리밍 지원
GET /health
생존 상태 확인
GET /ready
준비 상태 확인
모델 목록은 매번 Raycast에서 가져온다. 따라서 Raycast에 새 모델이 생겨도 이쪽 코드를 바꿀 필요가 없다. 가져온 뒤에는 환경 변수로 한 번 걸러 낸다. ADVANCED는 고급 모델을 포함할지, INCLUDE_DEPRECATED는 더 이상 권장되지 않는 모델을 포함할지 결정한다.
대화 변환도 복잡하지 않다. OpenAI 메시지 목록 맨 앞의 system 메시지는 Raycast의 system_instruction이 되고, user와 assistant 메시지는 Raycast의 author와 content.text 형식으로 바꾼다. 요청한 모델 ID를 모델 목록에서 찾아 provider와 내부 모델 이름을 구하고, 요청마다 새 thread_id도 생성한다.
Raycast는 SSE로 응답하며, 각 data: 줄에 짧은 텍스트와 종료 표시가 담긴다. 스트리밍에서는 각 조각을 OpenAI의 chat.completion.chunk로 다시 포장해 보내고, 마지막에 [DONE]을 덧붙인다. 비스트리밍에서는 모든 텍스트를 이어 붙여 한 번에 반환한다. Raycast가 token 사용량을 제공하지 않으므로 비스트리밍 응답의 usage는 일단 0으로 채우고, 스트리밍 응답에는 사용량 통계를 넣지 않는다.
V2 서명
새 Raycast 요청에는 기존 Authorization과 X-Raycast-DeviceId 외에 X-Raycast-Timestamp와 X-Raycast-Signature-v2가 추가됐다. 앞의 항목들은 간단하지만, 마지막 서명은 직접 계산해야 한다.
서명 과정은 몇 단계뿐이다. 먼저 요청 본문의 SHA-256 해시를 구한다. 그다음 타임스탬프, 기기 ID, 해시 값에 각각 ROT13과 ROT5 인코딩을 적용한다. 영문자는 ROT13, 숫자는 ROT5로 바꾸고 나머지 문자는 그대로 둔다. 이 세 부분을 .으로 연결한 다음, 비밀 키로 HMAC-SHA256을 계산한다. 그 결과를 16진수로 표현한 값이 서명이다.
타임스탬프와 기기 ID가 서명에 포함되므로 요청마다 서명이 달라지고, 기기 ID도 요청에 묶인다. 프로젝트에 기본 키가 들어 있지만, 나중에 바뀐다면 RAYCAST_SIGNATURE_SECRET 환경 변수로 덮어쓸 수 있다.
여기서는 모두 Web Crypto API를 사용하므로 추가 의존성 없이 같은 코드가 Workers와 Bun에서 동작한다.
코드 하나, 배포 방식 세 가지
가능한 한 쉽게 배포할 수 있게 만들고 싶었다. Cloudflare Workers에 익숙한 사람도, Docker를 선호하는 사람도, 자기 컴퓨터에서 잠깐 실행해 보고 싶은 사람도 있다. 그래서 세 가지 방식을 모두 지원하려 했다.
다행히 Hono는 여러 런타임을 지원하는 프레임워크다. 같은 app을 Worker 진입점으로 바로 쓰거나 Bun.serve에 넘길 수 있다. 환경 변수도 Hono의 env()로 읽으므로 어느 런타임이든 코드가 같다.
Cloudflare Workers
진입점은 src/index.ts이고, wrangler.jsonc에는 이름과 진입 파일만 적었다. README에 있는 Deploy to Cloudflare Workers 버튼을 누르면 배포할 수 있다. 이후 대시보드에서 환경 변수를 입력하면 된다.
Docker
server.ts는 몇 줄뿐이다. Hono의 app.fetch를 Bun.serve에 넘기며 기본 포트는 3000이다. Dockerfile은 두 단계로 나뉜다. 먼저 bun build --compile로 독립 실행 파일을 만들고, 다음 단계에서 이를 debian:bookworm-slim에 복사한다. 따라서 최종 이미지에는 바이너리 하나만 들어간다.
Release를 게시하면 GitHub Actions가 이미지를 자동으로 빌드해 ghcr.io에 푸시한다. linux/amd64와 linux/arm64를 모두 지원한다. Kubernetes에서는 /health를 라이브니스 프로브, /ready를 레디니스 프로브로 사용할 수 있다. 전자는 필요한 설정이 모두 있는지만 확인한다. 후자는 설정과 함께 최근 Raycast 요청의 성공 여부를 확인하고, 문제가 있으면 모델 목록을 가져와 Raycast에 연결되는지 점검한다. 이 점검은 최대 30초에 한 번만 실행한다.
Note
이 버전의 상태 프로브는 RAYCAST_SIGNATURE_SECRET이 설정되어 있는지 직접 확인한다. 서명 로직 자체에 기본값이 있더라도 프로브를 쓸 때는 이 항목을 명시적으로 설정해야 한다.
바이너리
Docker에서 쓰는 컴파일 명령의 --target만 바꾸면 다른 플랫폼용 버전도 만들 수 있다. Release를 게시할 때 Actions가 Linux, macOS, Windows와 x64, arm64 조합을 빌드하되 Windows arm64는 제외하고, 결과물을 Release에 업로드한다. 내려받아 바로 실행하면 되며 런타임을 따로 설치할 필요가 없다.
인증 정보
실행 전 필요한 값은 RAYCAST_BEARER_TOKEN과 RAYCAST_DEVICE_ID 두 개다. 둘 다 Raycast가 직접 보내는 요청에서 얻을 수 있다. Proxyman이나 Charles 같은 패킷 캡처 도구에서 backend.raycast.com의 SSL 프록시를 켠 뒤, Raycast에서 AI 대화 메시지를 아무거나 보내면 된다. 요청 헤더에서 Authorization: Bearer ...와 X-Raycast-DeviceId를 확인할 수 있다.
Warning
이 두 값은 사실상 자신의 Raycast 계정과 같으므로 절대 유출하면 안 된다. API_KEY를 설정하지 않으면 배포한 API가 누구에게나 열려 다른 사람도 내 계정으로 사용할 수 있다. 반드시 설정하는 것을 권한다. 클라이언트에서는 이 값을 OpenAI API Key로 쓰면 된다.
사용법
배포를 마쳤다면 OpenAI 주소를 직접 지정할 수 있는 클라이언트에서 Base URL을 배포 주소로, API Key를 자신이 설정한 API_KEY로 입력한다. 그러면 모델을 선택해 바로 대화할 수 있다.
Raycast에는 여러 모델을 쓸 수 있는 AI가 내장되어 있다. 이를 OpenAI 호환 API로 바꿔 다른 클라이언트에서도 바로 쓸 수 있게 해 주는 유용한 프로젝트가 두 개 있다. 하나는 raycast-relay, 다른 하나는 missuo의 raycast2api다. 나도 그동안 둘 다 사용했다.
그런데 Raycast 업데이트 후 두 프로젝트 모두 작동하지 않게 됐다. 새 요청에 서명이 추가됐고, 이 서명 없이는 요청이 통과하지 못하기 때문이었다. raycast-relay는 이미 보관 처리되어 더 이상 유지보수되지 않았다. 그래서 직접 새 서명을 역분석하고 다시 만들었다. 이름은 raycast2api를 그대로 썼고, 저장소는 xxxbrian/raycast2api에 있다.
하는 일
프로젝트가 하는 일은 사실 하나뿐이다. OpenAI 형식의 요청을 Raycast 요청으로 바꾸고, Raycast 응답을 다시 되돌린다. 외부에 제공하는 엔드포인트도 몇 개 없다.
엔드포인트
용도
GET /v1/models
모델 목록
POST /v1/chat/completions
대화, 스트리밍·비스트리밍 지원
GET /health
생존 상태 확인
GET /ready
준비 상태 확인
모델 목록은 매번 Raycast에서 가져온다. 따라서 Raycast에 새 모델이 생겨도 이쪽 코드를 바꿀 필요가 없다. 가져온 뒤에는 환경 변수로 한 번 걸러 낸다. ADVANCED는 고급 모델을 포함할지, INCLUDE_DEPRECATED는 더 이상 권장되지 않는 모델을 포함할지 결정한다.
대화 변환도 복잡하지 않다. OpenAI 메시지 목록 맨 앞의 system 메시지는 Raycast의 system_instruction이 되고, user와 assistant 메시지는 Raycast의 author와 content.text 형식으로 바꾼다. 요청한 모델 ID를 모델 목록에서 찾아 provider와 내부 모델 이름을 구하고, 요청마다 새 thread_id도 생성한다.
Raycast는 SSE로 응답하며, 각 data: 줄에 짧은 텍스트와 종료 표시가 담긴다. 스트리밍에서는 각 조각을 OpenAI의 chat.completion.chunk로 다시 포장해 보내고, 마지막에 [DONE]을 덧붙인다. 비스트리밍에서는 모든 텍스트를 이어 붙여 한 번에 반환한다. Raycast가 token 사용량을 제공하지 않으므로 비스트리밍 응답의 usage는 일단 0으로 채우고, 스트리밍 응답에는 사용량 통계를 넣지 않는다.
V2 서명
새 Raycast 요청에는 기존 Authorization과 X-Raycast-DeviceId 외에 X-Raycast-Timestamp와 X-Raycast-Signature-v2가 추가됐다. 앞의 항목들은 간단하지만, 마지막 서명은 직접 계산해야 한다.
서명 과정은 몇 단계뿐이다. 먼저 요청 본문의 SHA-256 해시를 구한다. 그다음 타임스탬프, 기기 ID, 해시 값에 각각 ROT13과 ROT5 인코딩을 적용한다. 영문자는 ROT13, 숫자는 ROT5로 바꾸고 나머지 문자는 그대로 둔다. 이 세 부분을 .으로 연결한 다음, 비밀 키로 HMAC-SHA256을 계산한다. 그 결과를 16진수로 표현한 값이 서명이다.
타임스탬프와 기기 ID가 서명에 포함되므로 요청마다 서명이 달라지고, 기기 ID도 요청에 묶인다. 프로젝트에 기본 키가 들어 있지만, 나중에 바뀐다면 RAYCAST_SIGNATURE_SECRET 환경 변수로 덮어쓸 수 있다.
여기서는 모두 Web Crypto API를 사용하므로 추가 의존성 없이 같은 코드가 Workers와 Bun에서 동작한다.
코드 하나, 배포 방식 세 가지
가능한 한 쉽게 배포할 수 있게 만들고 싶었다. Cloudflare Workers에 익숙한 사람도, Docker를 선호하는 사람도, 자기 컴퓨터에서 잠깐 실행해 보고 싶은 사람도 있다. 그래서 세 가지 방식을 모두 지원하려 했다.
다행히 Hono는 여러 런타임을 지원하는 프레임워크다. 같은 app을 Worker 진입점으로 바로 쓰거나 Bun.serve에 넘길 수 있다. 환경 변수도 Hono의 env()로 읽으므로 어느 런타임이든 코드가 같다.
Cloudflare Workers
진입점은 src/index.ts이고, wrangler.jsonc에는 이름과 진입 파일만 적었다. README에 있는 Deploy to Cloudflare Workers 버튼을 누르면 배포할 수 있다. 이후 대시보드에서 환경 변수를 입력하면 된다.
Docker
server.ts는 몇 줄뿐이다. Hono의 app.fetch를 Bun.serve에 넘기며 기본 포트는 3000이다. Dockerfile은 두 단계로 나뉜다. 먼저 bun build --compile로 독립 실행 파일을 만들고, 다음 단계에서 이를 debian:bookworm-slim에 복사한다. 따라서 최종 이미지에는 바이너리 하나만 들어간다.
Release를 게시하면 GitHub Actions가 이미지를 자동으로 빌드해 ghcr.io에 푸시한다. linux/amd64와 linux/arm64를 모두 지원한다. Kubernetes에서는 /health를 라이브니스 프로브, /ready를 레디니스 프로브로 사용할 수 있다. 전자는 필요한 설정이 모두 있는지만 확인한다. 후자는 설정과 함께 최근 Raycast 요청의 성공 여부를 확인하고, 문제가 있으면 모델 목록을 가져와 Raycast에 연결되는지 점검한다. 이 점검은 최대 30초에 한 번만 실행한다.
이 버전의 상태 프로브는 RAYCAST_SIGNATURE_SECRET이 설정되어 있는지 직접 확인한다. 서명 로직 자체에 기본값이 있더라도 프로브를 쓸 때는 이 항목을 명시적으로 설정해야 한다.
바이너리
Docker에서 쓰는 컴파일 명령의 --target만 바꾸면 다른 플랫폼용 버전도 만들 수 있다. Release를 게시할 때 Actions가 Linux, macOS, Windows와 x64, arm64 조합을 빌드하되 Windows arm64는 제외하고, 결과물을 Release에 업로드한다. 내려받아 바로 실행하면 되며 런타임을 따로 설치할 필요가 없다.
인증 정보
실행 전 필요한 값은 RAYCAST_BEARER_TOKEN과 RAYCAST_DEVICE_ID 두 개다. 둘 다 Raycast가 직접 보내는 요청에서 얻을 수 있다. Proxyman이나 Charles 같은 패킷 캡처 도구에서 backend.raycast.com의 SSL 프록시를 켠 뒤, Raycast에서 AI 대화 메시지를 아무거나 보내면 된다. 요청 헤더에서 Authorization: Bearer ...와 X-Raycast-DeviceId를 확인할 수 있다.
이 두 값은 사실상 자신의 Raycast 계정과 같으므로 절대 유출하면 안 된다. API_KEY를 설정하지 않으면 배포한 API가 누구에게나 열려 다른 사람도 내 계정으로 사용할 수 있다. 반드시 설정하는 것을 권한다. 클라이언트에서는 이 값을 OpenAI API Key로 쓰면 된다.
사용법
배포를 마쳤다면 OpenAI 주소를 직접 지정할 수 있는 클라이언트에서 Base URL을 배포 주소로, API Key를 자신이 설정한 API_KEY로 입력한다. 그러면 모델을 선택해 바로 대화할 수 있다.