Turning Raycast AI into an OpenAI-Compatible API

/ , , , , ,
Simplified ChineseEnglish
Summary:
After a Raycast update, the existing relay projects stopped working. I reverse-engineered the new request signature and built raycast2api, which runs on Cloudflare Workers, in Docker, or as a standalone binary.

Raycast has built-in AI with access to quite a few models. There are two useful projects that expose it as an OpenAI-compatible API, letting other clients use it directly. One is raycast-relay, and the other is missuo's raycast2api. I had been using both.

But after Raycast updated, neither project worked anymore. The new requests required an additional signature; without it, they would not go through. raycast-relay had already been archived and was no longer maintained, so I decided to do it myself. I reverse-engineered the new signature and wrote a replacement, keeping the name raycast2api. The repository is xxxbrian/raycast2api.

What it does

The project really does just one thing: convert OpenAI-format requests into Raycast requests, then convert the responses back. It exposes only a few endpoints.

Endpoint

Purpose

GET /v1/models

Model list

POST /v1/chat/completions

Chat, with streaming and non-streaming responses

GET /health

Liveness check

GET /ready

Readiness check

The model list is fetched from Raycast every time, so when Raycast adds a model, nothing needs changing here. After fetching it, I filter it using environment variables: ADVANCED controls whether advanced models are included, and INCLUDE_DEPRECATED controls whether deprecated ones are included.

Converting the conversation is straightforward too. A system message at the start of the OpenAI message list becomes Raycast's system_instruction. user and assistant messages are converted to Raycast's author plus content.text format. I look up the requested model ID in the model list to find its provider and internal model name, and generate a new thread_id for every request.

Raycast responds with SSE, with each data: line carrying a small piece of text and a finish marker. For streaming responses, I repackage each piece as an OpenAI chat.completion.chunk, send it out, and append [DONE] at the end. For non-streaming responses, I concatenate all the text and return it at once. Since Raycast does not report token usage, usage in non-streaming responses is currently set to 0; streaming responses do not provide usage statistics.

The V2 signature

Alongside the existing Authorization and X-Raycast-DeviceId, new Raycast requests include X-Raycast-Timestamp and X-Raycast-Signature-v2. The earlier fields are easy to handle. The tricky part is calculating that last signature myself.

The signing process takes just a few steps. First, calculate the SHA-256 hash of the request body. Then apply ROT13 plus ROT5 to the timestamp, device ID, and hash separately: ROT13 for letters, ROT5 for digits, and no change to other characters. Join the three parts with ., then use a secret key to calculate HMAC-SHA256. The resulting hexadecimal string is the signature.

Because both the timestamp and device ID are part of the signature, it changes with every request, and the device ID is bound to the request. The project includes a default key; if it changes later, it can be overridden with the RAYCAST_SIGNATURE_SECRET environment variable.

All of this uses Web Crypto APIs, so the same code runs on Workers and Bun without additional dependencies.

One codebase, three deployment options

I wanted to make the project as easy to deploy as possible. Some people are used to Cloudflare Workers, some prefer Docker, and others just want to run it on their own computer. I wanted to support all three.

Fortunately, Hono is a cross-runtime framework. The same app can serve directly as a Worker entry point or be passed to Bun.serve. Environment variables are also read through Hono's env(), so the code is the same in either runtime.

Mermaid diagram

Cloudflare Workers

The entry point is src/index.ts, and wrangler.jsonc contains just the name and entry file. The README has a Deploy to Cloudflare Workers button: click it to deploy, then fill in the environment variables in the dashboard.

Docker

server.ts is only a few lines long. It passes Hono's app.fetch to Bun.serve, with port 3000 as the default. The Dockerfile has two stages: the first uses bun build --compile to produce a standalone executable, and the second copies it into debian:bookworm-slim. The final image therefore contains just a single binary.

BASH
docker run -d \
  --name raycast2api \
  -p 3000:3000 \
  -e RAYCAST_BEARER_TOKEN=your-raycast-bearer-token \
  -e RAYCAST_DEVICE_ID=your-raycast-device-id \
  -e API_KEY=your-api-key \
  ghcr.io/xxxbrian/raycast2api:latest

When a Release is published, GitHub Actions automatically builds the image and pushes it to ghcr.io, supporting both linux/amd64 and linux/arm64. In Kubernetes, /health can be used as the liveness probe and /ready as the readiness probe. The former only checks that the required configuration is present. The latter also checks whether recent Raycast requests have succeeded; if not, it tries fetching the model list to see whether Raycast is reachable. This check runs at most once every 30 seconds.

Note

This version's health probes directly check whether RAYCAST_SIGNATURE_SECRET is configured. Even though the signing logic has a default value, this variable must be set explicitly when using the probes.

Binaries

Changing --target in the compilation command used by Docker produces builds for different platforms. When a Release is published, Actions builds the Linux, macOS, and Windows combinations of x64 and arm64, excluding Windows arm64, then uploads the results to the Release. Download a binary and run it directly; no runtime needs to be installed.

Credentials

Two values are needed before running it: RAYCAST_BEARER_TOKEN and RAYCAST_DEVICE_ID. Both can be obtained from requests made by Raycast itself. Use a traffic capture tool such as Proxyman or Charles, enable SSL proxying for backend.raycast.com, and send any AI chat message in Raycast. The request headers will contain Authorization: Bearer ... and X-Raycast-DeviceId.

Warning

These two values effectively grant access to your Raycast account. Do not leak them. Without API_KEY, your deployed API is open to everyone, and others can use your account through it. I strongly recommend setting it; clients can use it as their OpenAI API Key.

Using it

Once deployed, open any client that supports a custom OpenAI endpoint. Set its Base URL to the deployment URL and its API Key to the API_KEY you configured, then choose a model and start chatting.

  • Loading...
  • Loading...
  • Loading...
  • Loading...
  • Loading...