Skip to content
Merged
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ curl -sN localhost:8080/v1/messages \

# Your own config
GW_CONFIG=conf/gateway.yaml cargo run -p gw-server
gw --version # the built binary takes no other arguments

# Go live: give an account `endpoint` + `api_key_env` in the config — that's it.
# GW_TRANSPORT=mock forces zero egress; GW_TRANSPORT=http disables the mock.
Expand Down
14 changes: 14 additions & 0 deletions crates/server/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,26 @@ use tracing_subscriber::{EnvFilter, Layer as _};
const BATCH_STALE_SECS: i64 = 120;
const BATCH_POLL: Duration = Duration::from_secs(2);
const CONFIG_FEED_RETRY: Duration = Duration::from_secs(5);
const USAGE: &str = "usage: gw [--version | --help]\nconfiguration comes from the environment: GW_CONFIG, GW_TRANSPORT, GW_PORT";

#[global_allocator]
static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc;

#[tokio::main]
async fn main() -> anyhow::Result<()> {
if let Some(flag) = env::args().nth(1) {
return match flag.as_str() {
"--version" | "-V" => {
println!("gw {}", env!("CARGO_PKG_VERSION"));
Ok(())
}
"--help" | "-h" => {
println!("{USAGE}");
Ok(())
}
_ => Err(anyhow::anyhow!("unknown argument {flag}\n{USAGE}")),
};
}
let tracer_provider = init_tracing()?;

// reloads re-read this captured source
Expand Down
24 changes: 24 additions & 0 deletions crates/server/tests/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
use std::process::Command;

#[test]
fn version_flag_prints_the_version_and_exits() {
let out = Command::new(env!("CARGO_BIN_EXE_gw"))
.arg("--version")
.output()
.expect("run gw");
assert!(out.status.success());
assert_eq!(
String::from_utf8_lossy(&out.stdout).trim(),
format!("gw {}", env!("CARGO_PKG_VERSION"))
);
}

#[test]
fn an_unknown_argument_is_refused_with_usage() {
let out = Command::new(env!("CARGO_BIN_EXE_gw"))
.arg("--bogus")
.output()
.expect("run gw");
assert!(!out.status.success());
assert!(String::from_utf8_lossy(&out.stderr).contains("usage: gw"));
}
2 changes: 1 addition & 1 deletion docs/providers.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ models:
| `gemini` | `https://generativelanguage.googleapis.com` | gemini, realtime | `x-goog-api-key` (realtime = the Live API socket, live-verified: the bridge admits on `clientContent.turnComplete`, relays the binary frames, and settles `usageMetadata` — audio output tokens at their own weight) |
| `deepseek` | `https://api.deepseek.com` | openai-chat | `Bearer` |
| `openrouter` | `https://openrouter.ai/api` | openai-chat | `Bearer` (its `reasoning_details` shape is the one this gateway emits, so signed Anthropic reasoning round-trips through tool loops; verified live on free and paid models) |
| `moonshot` | `https://api.moonshot.cn` | openai-chat | `Bearer` (Kimi K2 thinking: `reasoning_content` in and out, `thinking: {type: disabled}` passes through; the vendor's `/anthropic` base also works as `kind: anthropic` + `endpoint`) |
| `moonshot` | `https://api.moonshot.cn` | openai-chat | `Bearer` (Kimi K2 thinking: `reasoning_content` in and out, `thinking: {type: disabled}` passes through; the vendor's `/anthropic` base also works as `kind: anthropic` + `endpoint`; the international site is `endpoint: https://api.moonshot.ai` — keys are site-specific and kimi-k3 is only there) |
| `xai` | `https://api.x.ai` | openai-chat, responses, image, video, realtime | `Bearer` (Grok: `reasoning_effort` per model — grok-4.6 `low`–`xhigh`, grok-4.3 also `none`, others reject values they don't list; usage carries `prompt_tokens_details.cached_tokens` and `completion_tokens_details.reasoning_tokens`; xAI's own Anthropic-compatible surface is deprecated, so Anthropic clients reach Grok through this gateway's `/v1/messages` cross-protocol path; verified live: grok-4.3/4.6/4.20 chat + stream + effort tiers, `/v1/messages` both ways, grok-4.5 through `protocol: responses` natively and from the chat/messages surfaces incl. tool loops, grok-imagine-image-2.0 per-image units, and grok-voice-latest through `/v1/realtime` (`protocol: realtime`; xAI's `response.done` carries an empty `usage`, so the turn bills the delivered-output estimate — transcript tokens plus audio bytes/4 — as `estimated`, input unmetered: price the model per output unit accordingly); grok-imagine-video-1.5 through `/v1/videos/generations` + `GET /v1/videos/{id}` (`protocol: video`, unit price per generated second, vendor cost from `usage.cost_in_usd_ticks`); files/collections and vendor batches are not wired) |
| `siliconflow` | `https://api.siliconflow.cn` | openai-chat, embeddings, rerank, tts, stt, image, video | `Bearer` (Qwen3 `enable_thinking`, DeepSeek/GLM/Kimi/MiniMax hosted models, bge/Qwen3 embeddings and rerankers, CosyVoice TTS, SenseVoice STT, Kolors images — all verified live) |

Expand Down