Skip to content

Commit 674e766

Browse files
Darth-Hidiousclaude
andcommitted
fix: resolve 6 CLI-side API issues from platform analysis
- #3: Handle missing usage in Google SSE (fallback to input_tokens/output_tokens) - #4: Fix support ticket URL double /api/v1, use correct field names - #5: Add retry-with-backoff for upstream 429s in LLM client - #6: Fix README discourse events → turns - #9: Skip TUI launch if binary not found (check first, don't try/catch) - Marketplace: align struct with actual API response fields 571 tests pass, 0 clippy warnings. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 49e9b04 commit 674e766

3 files changed

Lines changed: 71 additions & 37 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,7 @@ prism models info <model-id> # Show model details
131131
prism discourse create --spec spec.yaml # Create a discourse
132132
prism discourse list # List discourses
133133
prism discourse status <id> # Check discourse status
134-
prism discourse events <id> # Stream discourse events
134+
prism discourse turns <id> # View discourse turns
135135
```
136136

137137
### Deploy

crates/cli/src/main.rs

Lines changed: 21 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -751,19 +751,18 @@ async fn main() -> Result<()> {
751751
}
752752
}
753753
}
754-
// TUI is currently out of commission — guide user to CLI commands
755-
match launch_tui(&paths, &python, &project_root, state.credentials.as_ref()) {
756-
Ok(()) => {}
757-
Err(_) => {
758-
println!("\n PRISM v{}", env!("CARGO_PKG_VERSION"));
759-
println!(" The interactive TUI is not available in this build.\n");
760-
println!(" Use CLI commands directly:");
761-
println!(" prism query --platform \"nickel superalloys\"");
762-
println!(" prism research \"high entropy alloys\" --depth 1");
763-
println!(" prism ingest ./data.csv");
764-
println!(" prism models list");
765-
println!(" prism --help\n");
766-
}
754+
// Skip TUI launch entirely if binary doesn't exist
755+
if discover_tui_binary(&paths).is_some() {
756+
launch_tui(&paths, &python, &project_root, state.credentials.as_ref())?;
757+
} else {
758+
println!("\n PRISM v{}", env!("CARGO_PKG_VERSION"));
759+
println!(" The interactive TUI is not available in this build.\n");
760+
println!(" Use CLI commands directly:");
761+
println!(" prism query --platform \"nickel superalloys\"");
762+
println!(" prism research \"high entropy alloys\" --depth 1");
763+
println!(" prism ingest ./data.csv");
764+
println!(" prism models list");
765+
println!(" prism --help\n");
767766
}
768767
}
769768
Commands::Login => {
@@ -1808,11 +1807,6 @@ async fn main() -> Result<()> {
18081807
} else {
18091808
println!("Marketplace resources:\n");
18101809
for t in &tools {
1811-
let tags = if t.tags.is_empty() {
1812-
String::new()
1813-
} else {
1814-
format!(" [{}]", t.tags.join(", "))
1815-
};
18161810
let author = t.author.as_deref().unwrap_or("MARC27");
18171811
println!(
18181812
" {} ({}) by {} [{}]",
@@ -5321,8 +5315,8 @@ async fn handle_report(
53215315
let user_name = creds
53225316
.and_then(|c| c.display_name.as_deref())
53235317
.unwrap_or("anonymous");
5324-
let user_id = creds.and_then(|c| c.user_id.as_deref()).unwrap_or("");
5325-
let project_id = creds.and_then(|c| c.project_id.as_deref()).unwrap_or("");
5318+
let _user_id = creds.and_then(|c| c.user_id.as_deref()).unwrap_or("");
5319+
let _project_id = creds.and_then(|c| c.project_id.as_deref()).unwrap_or("");
53265320

53275321
// 2. Build the report body
53285322
let mut body = format!(
@@ -5387,20 +5381,15 @@ async fn handle_report(
53875381
if !c.access_token.is_empty() {
53885382
print!("Sending to MARC27 platform... ");
53895383
let platform_body = serde_json::json!({
5390-
"type": "bug_report",
5391-
"description": description,
5392-
"prism_version": version,
5393-
"python_version": python_version,
5394-
"os": os_info,
5395-
"cpu_cores": caps.cpu_cores,
5396-
"ram_gb": caps.ram_gb / 1024,
5397-
"docker": caps.docker,
5398-
"log": log_content,
5399-
"user_id": user_id,
5400-
"project_id": project_id,
5384+
"title": format!("bug report: {}", &description[..description.len().min(60)]),
5385+
"description": format!(
5386+
"{description}\n\nPRISM v{version}, Python {python_version}, {os_info}, {} cores, {} GB RAM",
5387+
caps.cpu_cores, caps.ram_gb / 1024,
5388+
),
5389+
"severity": "medium",
54015390
});
54025391

5403-
let url = format!("{}/api/v1/support/tickets", endpoints.api_base);
5392+
let url = format!("{}/support/tickets", endpoints.api_base);
54045393
let resp = reqwest::Client::new()
54055394
.post(&url)
54065395
.header("Authorization", format!("Bearer {}", c.access_token))

crates/ingest/src/llm.rs

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -297,10 +297,17 @@ impl LlmClient {
297297
}
298298
}
299299
if let Some(u) = chunk.get("usage") {
300-
if let (Some(pt), Some(ct)) = (
301-
u.get("prompt_tokens").and_then(|v| v.as_u64()),
302-
u.get("completion_tokens").and_then(|v| v.as_u64()),
303-
) {
300+
let pt = u
301+
.get("prompt_tokens")
302+
.or_else(|| u.get("input_tokens"))
303+
.and_then(|v| v.as_u64())
304+
.unwrap_or(0);
305+
let ct = u
306+
.get("completion_tokens")
307+
.or_else(|| u.get("output_tokens"))
308+
.and_then(|v| v.as_u64())
309+
.unwrap_or(0);
310+
if pt > 0 || ct > 0 {
304311
usage_info = Some(UsageInfo {
305312
prompt_tokens: pt,
306313
completion_tokens: ct,
@@ -500,6 +507,44 @@ impl LlmClient {
500507

501508
async fn post(&self, url: &str, body: &serde_json::Value) -> Result<reqwest::Response> {
502509
debug!(%url, "LLM request");
510+
for attempt in 0..3u32 {
511+
let mut req = self.client.post(url).json(body);
512+
if let Some(auth) = self.auth_header() {
513+
req = req.header("Authorization", auth);
514+
}
515+
let resp = req
516+
.send()
517+
.await
518+
.with_context(|| format!("LLM request to {url} failed"))?;
519+
if resp.status() == reqwest::StatusCode::TOO_MANY_REQUESTS {
520+
let wait = resp
521+
.headers()
522+
.get("retry-after")
523+
.and_then(|v| v.to_str().ok())
524+
.and_then(|s| s.parse::<u64>().ok())
525+
.unwrap_or(2u64.pow(attempt));
526+
debug!(attempt, wait_secs = wait, "429 — retrying after backoff");
527+
tokio::time::sleep(Duration::from_secs(wait)).await;
528+
continue;
529+
}
530+
if !resp.status().is_success() {
531+
let status = resp.status();
532+
let text = resp.text().await.unwrap_or_default();
533+
bail!("LLM returned HTTP {status}: {text}");
534+
}
535+
return Ok(resp);
536+
}
537+
bail!("LLM request to {url} failed after 3 retries (429 rate limit)");
538+
}
539+
540+
// Keep old signature for callers that held the single-attempt path
541+
#[allow(dead_code)]
542+
async fn post_no_retry(
543+
&self,
544+
url: &str,
545+
body: &serde_json::Value,
546+
) -> Result<reqwest::Response> {
547+
debug!(%url, "LLM request (no retry)");
503548
let mut req = self.client.post(url).json(body);
504549
if let Some(auth) = self.auth_header() {
505550
req = req.header("Authorization", auth);

0 commit comments

Comments
 (0)