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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).

### Added

- feat(cli): `zeph gonka doctor` diagnostic subcommand — checks vault key resolution, signer
construction, and per-node HTTP reachability with signed probes. Reports `[OK]`, `[WARN]`, or
`[FAIL]` per check; detects 401 clock skew via `Date` header. `--json` flag emits a structured
JSON envelope. Requires `gonka` feature. Also adds an `#[ignore]` live-testnet integration test
in `crates/zeph-llm/tests/gonka_live.rs`. (#3614)

- feat(core): `SpeculationEngine.try_dispatch` wired into two activation paths. SSE decoding path:
`claude_sse_to_tool_stream` emits `ToolBlockStart` at `content_block_start` so
`SpeculativeStreamDrainer` can populate `tool_meta` before `InputJsonDelta` events arrive; when
Expand Down
10 changes: 10 additions & 0 deletions book/src/guides/gonka.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,16 @@ address = "gonka1..."

## Troubleshooting

Run the built-in diagnostic tool to check credentials and node reachability:

```bash
zeph gonka doctor
# or for machine-readable JSON output:
zeph gonka doctor --json
```

The doctor prints `[OK]`, `[WARN]`, or `[FAIL]` for each check: vault key resolution, signer construction, and per-node HTTP probes with latency. Exit code is 0 on success, 1 on failures.

| Symptom | Cause | Fix |
|---------|-------|-----|
| 401 / signature error | Invalid key format or address mismatch | Verify `ZEPH_GONKA_PRIVATE_KEY` is hex-encoded secp256k1; confirm address matches key |
Expand Down
62 changes: 62 additions & 0 deletions crates/zeph-llm/tests/gonka_live.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
// SPDX-License-Identifier: MIT OR Apache-2.0

//! Live testnet integration test for the Gonka provider.
//!
//! Skipped by default — requires a running Gonka testnet node and a funded wallet.
//! Run with:
//! ```shell
//! ZEPH_GONKA_PRIVATE_KEY=<hex> cargo nextest run -p zeph-llm -- --ignored gonka_live
//! ```

#[cfg(feature = "gonka")]
mod live {
use std::sync::Arc;
use std::time::Duration;

use zeph_llm::gonka::endpoints::{EndpointPool, GonkaEndpoint};
use zeph_llm::gonka::{GonkaProvider, RequestSigner};
use zeph_llm::provider::{LlmProvider, Message, Role};

#[tokio::test]
#[ignore = "requires ZEPH_GONKA_PRIVATE_KEY env var and live Gonka testnet access"]
async fn gonka_live_chat_round_trip() {
let priv_key = match std::env::var("ZEPH_GONKA_PRIVATE_KEY") {
Ok(k) if !k.is_empty() => k,
_ => {
eprintln!("ZEPH_GONKA_PRIVATE_KEY not set, skipping");
return;
}
};

let node_url = std::env::var("ZEPH_GONKA_NODE_URL")
.unwrap_or_else(|_| "http://node1.gonka.ai:8000".into());

let signer = Arc::new(
RequestSigner::from_hex(&priv_key, "gonka").expect("valid secp256k1 private key"),
);

let pool = Arc::new(
EndpointPool::new(vec![GonkaEndpoint {
base_url: node_url.clone(),
address: signer.address().to_owned(),
}])
.expect("non-empty pool"),
);

let provider =
GonkaProvider::new(signer, pool, "gpt-4o", 16, None, Duration::from_secs(30));

let messages = vec![Message::from_legacy(
Role::User,
"Say hello in one word.".to_owned(),
)];

let response = provider
.chat(&messages)
.await
.expect("chat should succeed against live testnet");

assert!(!response.is_empty(), "response must not be empty");
}
}
21 changes: 21 additions & 0 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -397,6 +397,12 @@ pub(crate) enum Command {
#[arg(long, default_value = "5")]
mcp_timeout_secs: u64,
},
/// Gonka network diagnostics and credential checks
#[cfg(feature = "gonka")]
Gonka {
#[command(subcommand)]
command: GonkaCommand,
},
/// Test notification channels (sends a test notification via enabled channels)
Notify {
#[command(subcommand)]
Expand Down Expand Up @@ -665,6 +671,21 @@ pub(crate) enum RouterCommand {
},
}

/// Gonka network subcommands.
#[cfg(feature = "gonka")]
#[derive(Subcommand)]
pub(crate) enum GonkaCommand {
/// Run Gonka connectivity and credential diagnostics
Doctor {
/// Emit results as JSON (`schema_version` = 1)
#[arg(long)]
json: bool,
/// Timeout in seconds for node probe requests
#[arg(long, default_value = "10")]
timeout_secs: u64,
},
}

/// Notification management subcommands.
#[derive(Subcommand)]
pub(crate) enum NotifyCommand {
Expand Down
16 changes: 12 additions & 4 deletions src/commands/doctor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ pub(crate) struct CheckResult {
}

impl CheckResult {
fn ok(name: impl Into<String>, detail: impl Into<String>, elapsed_ms: u64) -> Self {
pub(crate) fn ok(name: impl Into<String>, detail: impl Into<String>, elapsed_ms: u64) -> Self {
Self {
name: name.into(),
status: CheckStatus::Ok,
Expand All @@ -63,7 +63,11 @@ impl CheckResult {
}
}

fn warn(name: impl Into<String>, detail: impl Into<String>, elapsed_ms: u64) -> Self {
pub(crate) fn warn(
name: impl Into<String>,
detail: impl Into<String>,
elapsed_ms: u64,
) -> Self {
Self {
name: name.into(),
status: CheckStatus::Warn,
Expand All @@ -72,7 +76,11 @@ impl CheckResult {
}
}

fn fail(name: impl Into<String>, detail: impl Into<String>, elapsed_ms: u64) -> Self {
pub(crate) fn fail(
name: impl Into<String>,
detail: impl Into<String>,
elapsed_ms: u64,
) -> Self {
Self {
name: name.into(),
status: CheckStatus::Fail,
Expand Down Expand Up @@ -421,7 +429,7 @@ async fn check_llm_provider(
if entry.provider_type == ProviderKind::Gonka {
return CheckResult::ok(
&check_name,
"gonka (not yet implemented)",
"gonka (use `zeph gonka doctor` for detailed diagnostics)",
elapsed_ms(start),
);
}
Expand Down
Loading
Loading