diff --git a/README.md b/README.md index 1f09893..90e14b4 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/crates/server/src/main.rs b/crates/server/src/main.rs index 6b1b4d4..52d5dc4 100644 --- a/crates/server/src/main.rs +++ b/crates/server/src/main.rs @@ -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 diff --git a/crates/server/tests/cli.rs b/crates/server/tests/cli.rs new file mode 100644 index 0000000..25cbaca --- /dev/null +++ b/crates/server/tests/cli.rs @@ -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")); +} diff --git a/docs/providers.md b/docs/providers.md index 3e30ad9..a9f75ee 100644 --- a/docs/providers.md +++ b/docs/providers.md @@ -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) |