Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ ANTHROPIC_API_KEY=
XAI_API_KEY=
ARK_API_KEY=
OPENROUTER_API_KEY=
WAVESPEED_API_KEY=

# Blockchain heartbeat configuration (optional).
# If HEARTBEAT_CONTRACT_ADDRESS and HEARTBEAT_FACILITATOR_URL are set, the enclave
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ jobs:
# accept either secret name so the job runs whichever the repo still holds.
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY || secrets.NOUS_API_KEY }}
ZAI_API_KEY: ${{ secrets.ZAI_API_KEY }}
WAVESPEED_API_KEY: ${{ secrets.WAVESPEED_API_KEY }}
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v5
Expand All @@ -49,7 +50,7 @@ jobs:
run: |
for name in \
OPENAI_API_KEY ANTHROPIC_API_KEY GOOGLE_API_KEY XAI_API_KEY \
ARK_API_KEY OPENROUTER_API_KEY ZAI_API_KEY
ARK_API_KEY OPENROUTER_API_KEY ZAI_API_KEY WAVESPEED_API_KEY
do
if [[ -z "${!name}" ]]; then
echo "Missing required repository secret: $name" >&2
Expand Down
34 changes: 29 additions & 5 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ API keys (injected at runtime via POST /v1/keys — do NOT bake into the image):
- `ARK_API_KEY` (BytePlus / ByteDance ModelArk; injected as `bytedance_api_key`)
- `OPENROUTER_API_KEY` (OpenRouter; injected as `openrouter_api_key`)
- `ZAI_API_KEY` (Z.ai Model API; injected as `zai_api_key`)
- `WAVESPEED_API_KEY` (WaveSpeed; injected as `wavespeed_api_key`)
- `EXA_API_KEY` (Exa search; injected as `exa_api_key`) — backs the in-enclave
`/v1/web_search` endpoint, not an LLM provider. Without it the endpoint
returns 503 and `/health` reports `web_search_enabled: false`.
Expand Down Expand Up @@ -126,13 +127,14 @@ Server configuration:

`model_registry.py` is the source of truth; this list mirrors it. Model name
prefixes determine routing:
- **OpenAI**: gpt-6.1-sol, gpt-6-sol, gpt-6-luna, gpt-6-astra, gpt-4.1, gpt-4.1-mini, gpt-5, gpt-5-mini, gpt-5.2, gpt-5.4, gpt-5.4-mini, gpt-5.4-nano, gpt-5.5, gpt-5.6-sol/terra/luna, o3; image generation: gpt-image-2.5-flare, gpt-image-2.5-sunburst, gpt-image-2
- **Anthropic**: claude-sonnet-4-5/4-6, claude-sonnet-5, claude-sonnet-5-5, claude-haiku-4-5, claude-opus-4-5/4-6/4-7/4-8, claude-opus-5, claude-opus-5-5, claude-fable-5, claude-fable-5-1
- **Google**: gemini-3.8-flash, gemini-3.7-flash, gemini-3.6-flash, gemini-3.5-flash, gemini-3.5-flash-lite, gemini-2.5-flash, gemini-2.5-flash-lite, gemini-2.5-pro, gemini-3-flash-preview, gemini-3.1-pro-preview; image generation: gemini-2.5-flash-image, gemini-3.1-flash-image
- **OpenAI**: gpt-6.1-terra, gpt-6.1-sol, gpt-6-sol, gpt-6-luna, gpt-6-astra, gpt-4.1, gpt-4.1-mini, gpt-5, gpt-5-mini, gpt-5.2, gpt-5.4, gpt-5.4-mini, gpt-5.4-nano, gpt-5.5, gpt-5.6-sol/terra/luna, o3; image generation: gpt-image-2.5-flare, gpt-image-2.5-sunburst, gpt-image-2
- **Anthropic**: claude-sonnet-4-5/4-6, claude-sonnet-5, claude-sonnet-5-5, claude-haiku-5-5, claude-haiku-4-5, claude-opus-4-5/4-6/4-7/4-8, claude-opus-5, claude-opus-5-5, claude-fable-5, claude-fable-5-1
- **Google**: gemini-4-argon, gemini-3.8-flash, gemini-3.7-flash, gemini-3.6-flash, gemini-3.5-flash, gemini-3.5-flash-lite, gemini-2.5-flash, gemini-2.5-flash-lite, gemini-2.5-pro, gemini-3-flash-preview, gemini-3.1-pro-preview; image generation: gemini-2.5-flash-image, gemini-3.1-flash-image, gemini-nano-banana-2.1
- **xAI**: grok-4.7, grok-4.6, grok-4.5, grok-4.3, grok-4.20-reasoning, grok-4.20-non-reasoning; image generation: grok-2-image, grok-imagine-image-2.0
- **ByteDance** (BytePlus ModelArk, OpenAI-compatible, ap-southeast): seed-1.6, seed-1.8, seed-2.0-lite, deepseek-v4-flash, deepseek-v4-pro, glm-5.2 (Z.ai's model served via a ModelArk deployment endpoint); image generation: seedream-4.0, seedream-5.0-lite, seedance-4.5, seedance-5.0
- **OpenRouter** (OpenAI-compatible): hy4-preview, hermes-4-405b, hy3
- **Z.ai** (Model API, OpenAI-compatible): image generation: glm-image (glm-5.2 chat is routed through BytePlus ModelArk, see ByteDance above)
- **WaveSpeed** (async prediction API): image generation: krea-3 (Krea's model, always `berry` at `reasoning_level: medium`), qwen-image-3.0-pro (Alibaba's model); each is two WaveSpeed endpoints, text-to-image and edit

Models kept registered but no longer offered to new clients — each still
resolves so older SDK versions keep working, and each is priced at what the
Expand Down Expand Up @@ -181,8 +183,30 @@ prices by output pixel count can declare **resolution tiers**
`resolution` field (`"1.5K"`, `"2K"`) picks the tier, which supplies the
`size` keyword, the ratio→pixels table, and the per-image price — omitted
means the default tier, and the field is rejected on single-resolution
models. Seedream 5.0 is the only tiered model today (`1.5K` at $0.045 is the
default; `2K` at $0.09 is what every request used to be pinned to).
models. Seedream 5.0 (`1.5K` at $0.045 is the default; `2K` at $0.09 is what
every request used to be pinned to), Krea 3 (`1K` $0.03 default, `2K` $0.12,
each reference billed at the same price as the output, per WaveSpeed's
`$0.03 x MP x (1 + references)` formula) and Qwen Image 3.0 Pro (`1K` $0.04 default,
`2K` $0.075) are the tiered models today.

**WaveSpeed** (krea-3, qwen-image-3.0-pro) is the one image provider that does not
answer in a single call. `image_generation._generate_wavespeed` submits an
async prediction to `/{model_id}` — the `…/text-to-image` model, or the
`…/edit` model (`image_edit_model`) when the turn carries references — then
polls `/predictions/{id}/result` (2s, backing off to 5s, 180s deadline → 504,
under the relay's 210s read timeout),
fetches the output URL like any hosted image, and finally deletes the
prediction so WaveSpeed's history no longer holds the prompt or output.
WaveSpeed takes references as URLs only: inline `data:` references are
uploaded through its `/media/uploads` ticket (a keyless PUT to the signed
URL, which is a credential and is never logged), plain URLs are passed
through, and at most `image_max_references` (Qwen 3, Krea 8) are sent. Its
input images are billed too — `per_reference_image_price_usd` (Qwen $0.003
each; Krea per tier, at the output's price) on top of the per-image price,
counted from the references actually forwarded. One image per prediction; `n`
is ignored. Every Qwen request sends `enable_prompt_expansion: false`, and
every Krea request `model: berry` + `reasoning_level: medium`
(`image_extra_params`).

### Web Search

Expand Down
16 changes: 9 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,17 @@ The gateway solves this by running inside a hardware-isolated Nitro Enclave wher

| Provider | Models |
|----------|--------|
| OpenAI | gpt-6.1-sol, gpt-6-sol, gpt-6-luna, gpt-6-astra, gpt-4.1, gpt-4.1-mini, gpt-5, gpt-5-mini, gpt-5.2, gpt-5.4, gpt-5.5, gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, o3 |
| Anthropic | claude-sonnet-5-5, claude-fable-5-1, claude-fable-5, claude-opus-5-5, claude-opus-5, claude-opus-4-8, claude-opus-4-7, claude-opus-4-6, claude-opus-4-5, claude-sonnet-5, claude-sonnet-4-6, claude-sonnet-4-5, claude-haiku-4-5 |
| Google | gemini-3.8-flash, gemini-3.7-flash, gemini-3.6-flash, gemini-3.5-flash, gemini-3.5-flash-lite, gemini-2.5-flash, gemini-2.5-flash-lite, gemini-2.5-pro, gemini-3-flash-preview, gemini-3.1-pro-preview |
| OpenAI | gpt-6.1-terra, gpt-6.1-sol, gpt-6-sol, gpt-6-luna, gpt-6-astra, gpt-4.1, gpt-4.1-mini, gpt-5, gpt-5-mini, gpt-5.2, gpt-5.4, gpt-5.5, gpt-5.6-sol, gpt-5.6-terra, gpt-5.6-luna, o3 |
| Anthropic | claude-haiku-5-5, claude-sonnet-5-5, claude-fable-5-1, claude-fable-5, claude-opus-5-5, claude-opus-5, claude-opus-4-8, claude-opus-4-7, claude-opus-4-6, claude-opus-4-5, claude-sonnet-5, claude-sonnet-4-6, claude-sonnet-4-5, claude-haiku-4-5 |
| Google | gemini-4-argon, gemini-3.8-flash, gemini-3.7-flash, gemini-3.6-flash, gemini-3.5-flash, gemini-3.5-flash-lite, gemini-2.5-flash, gemini-2.5-flash-lite, gemini-2.5-pro, gemini-3-flash-preview, gemini-3.1-pro-preview |
| xAI | grok-4.7, grok-4.6, grok-4.5, grok-4.3, grok-4.20-reasoning, grok-4.20-non-reasoning |
| ByteDance | seed-1.6, seed-1.8, seed-2.0-lite, dola-seed-2.0-mini, deepseek-v4-flash, deepseek-v4-pro, glm-5.2 |
| OpenRouter | hy4-preview, hermes-4-405b, hy3 |

Image generation: gpt-image-2, gpt-image-2.5-flare, gpt-image-2.5-sunburst,
gemini-2.5-flash-image, gemini-3.1-flash-image, grok-2-image,
grok-imagine-image-2.0, seedream-4.0, seedream-5.0-lite, seedance-4.5,
seedance-5.0, glm-image.
Image generation: krea-3 and qwen-image-3.0-pro (via WaveSpeed), gpt-image-2,
gpt-image-2.5-flare, gpt-image-2.5-sunburst, gemini-2.5-flash-image,
gemini-nano-banana-2.1, gemini-3.1-flash-image, grok-2-image, grok-imagine-image-2.0, seedream-4.0,
seedream-5.0-lite, seedance-4.5, seedance-5.0, glm-image.

Some retired models stay registered so older SDK versions keep working; see
"Supported Providers" in CLAUDE.md. `tee_gateway/model_registry.py` is the
Expand All @@ -65,6 +65,7 @@ export XAI_API_KEY=...
export ARK_API_KEY=... # BytePlus / ByteDance ModelArk
export OPENROUTER_API_KEY=... # OpenRouter
export ZAI_API_KEY=... # Z.ai Model API
export WAVESPEED_API_KEY=... # WaveSpeed (krea-3, qwen-image-3.0-pro)

# Run server (starts the Flask/connexion app on port 8000)
make test-local # Werkzeug dev server (development only)
Expand Down Expand Up @@ -418,6 +419,7 @@ Clients use an x402-compatible client (e.g., the [x402 SDK](https://github.com/o
| `ARK_API_KEY` | - | BytePlus / ByteDance ModelArk API key (injected as `bytedance_api_key`) |
| `OPENROUTER_API_KEY` | - | OpenRouter API key (injected as `openrouter_api_key`) |
| `ZAI_API_KEY` | - | Z.ai Model API key (injected as `zai_api_key`) |
| `WAVESPEED_API_KEY` | - | WaveSpeed API key (injected as `wavespeed_api_key`) |
| `EVM_PAYMENT_ADDRESS` | - | Wallet address to receive x402 payments |
| `FACILITATOR_URL` | see `tee_gateway/__main__.py` | x402 payment facilitator endpoint |

Expand Down
5 changes: 4 additions & 1 deletion scripts/run-enclave.sh
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ if [ -f "$ENV_FILE" ]; then
ARK_API_KEY="$(grep -E '^ARK_API_KEY=' "$ENV_FILE" | cut -d'=' -f2-)"
OPENROUTER_API_KEY="$(grep -E '^OPENROUTER_API_KEY=' "$ENV_FILE" | cut -d'=' -f2-)"
ZAI_API_KEY="$(grep -E '^ZAI_API_KEY=' "$ENV_FILE" | cut -d'=' -f2-)"
WAVESPEED_API_KEY="$(grep -E '^WAVESPEED_API_KEY=' "$ENV_FILE" | cut -d'=' -f2-)"
# Backs the in-enclave `web_search` tool (Exa), not an LLM provider.
EXA_API_KEY="$(grep -E '^EXA_API_KEY=' "$ENV_FILE" | cut -d'=' -f2-)"

Expand All @@ -112,6 +113,7 @@ if [ -f "$ENV_FILE" ]; then
--arg bytedance "$ARK_API_KEY" \
--arg openrouter "$OPENROUTER_API_KEY" \
--arg zai "$ZAI_API_KEY" \
--arg wavespeed "$WAVESPEED_API_KEY" \
--arg exa "$EXA_API_KEY" \
--arg hb_contract "$HEARTBEAT_CONTRACT_ADDRESS" \
--arg facilitator "$FACILITATOR_URL" \
Expand All @@ -124,6 +126,7 @@ if [ -f "$ENV_FILE" ]; then
bytedance_api_key: $bytedance,
openrouter_api_key: $openrouter,
zai_api_key: $zai,
wavespeed_api_key: $wavespeed,
exa_api_key: $exa
}
+ if $hb_contract != "" then {heartbeat_contract_address: $hb_contract} else {} end
Expand Down Expand Up @@ -155,7 +158,7 @@ if [ -f "$ENV_FILE" ]; then

# Clear key variables from this shell immediately after use
unset OPENAI_API_KEY GOOGLE_API_KEY ANTHROPIC_API_KEY XAI_API_KEY ARK_API_KEY
unset OPENROUTER_API_KEY ZAI_API_KEY EXA_API_KEY
unset OPENROUTER_API_KEY ZAI_API_KEY WAVESPEED_API_KEY EXA_API_KEY
unset HEARTBEAT_CONTRACT_ADDRESS FACILITATOR_URL TEE_HEARTBEAT_INTERVAL
fi
else
Expand Down
6 changes: 6 additions & 0 deletions tee_gateway/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,7 @@ def set_provider_keys():
bytedance_api_key=body.get("bytedance_api_key") or None,
openrouter_api_key=body.get("openrouter_api_key") or None,
zai_api_key=body.get("zai_api_key") or None,
wavespeed_api_key=body.get("wavespeed_api_key") or None,
exa_api_key=body.get("exa_api_key") or None,
)
set_provider_config(provider_config)
Expand Down Expand Up @@ -496,6 +497,10 @@ def _set(val: str | None) -> str:
logger.info(
" zai_api_key : %s", _set(provider_config.zai_api_key)
)
logger.info(
" wavespeed_api_key : %s",
_set(provider_config.wavespeed_api_key),
)
logger.info(
" exa_api_key (web search) : %s", _set(provider_config.exa_api_key)
)
Expand Down Expand Up @@ -533,6 +538,7 @@ def _set(val: str | None) -> str:
"bytedance": provider_config.bytedance_api_key,
"openrouter": provider_config.openrouter_api_key,
"zai": provider_config.zai_api_key,
"wavespeed": provider_config.wavespeed_api_key,
}.items()
if k
]
Expand Down
3 changes: 3 additions & 0 deletions tee_gateway/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ class ProviderConfig:
bytedance_api_key: Optional[str] = None
openrouter_api_key: Optional[str] = None
zai_api_key: Optional[str] = None
wavespeed_api_key: Optional[str] = None
# Exa, which backs the `web_search` tool the gateway executes inside the
# enclave for every model (see web_search.py). Not an LLM provider, so it is
# deliberately absent from initialized_providers() — /health reports it
Expand All @@ -53,6 +54,8 @@ def initialized_providers(self) -> list[str]:
providers.append("openrouter")
if self.zai_api_key:
providers.append("zai")
if self.wavespeed_api_key:
providers.append("wavespeed")
return providers


Expand Down
Loading
Loading