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: 5 additions & 1 deletion src/openhuman/embeddings/openai.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,11 @@ impl EmbeddingProvider for OpenAiEmbedding {
"[openai] embed error: status={status}, body={text}"
);
let message = format!("Embedding API error {status}: {text}");
crate::core::observability::report_error(
// Route through the expected-error classifier so user-state
// conditions (budget exhausted / insufficient credits, missing
// API key, transient upstream HTTP) are demoted to info/warn
// breadcrumbs instead of spawning Sentry error events.
crate::core::observability::report_error_or_expected(
message.as_str(),
"embeddings",
"openai_embed",
Expand Down
26 changes: 26 additions & 0 deletions src/openhuman/embeddings/openai_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,32 @@ async fn embed_server_error() {
assert!(msg.contains("rate limited"), "body: {msg}");
}

#[tokio::test]
async fn embed_budget_exhausted_400_still_errors() {
// OPENHUMAN-TAURI-JM: the backend returns HTTP 400 with a budget-exhausted
// body when the user is out of credits. The provider must still surface
// an `Err` to the caller (so the calling pipeline can short-circuit), but
// the diagnostic emit site must route through `report_error_or_expected`
// so the message is classified as `BudgetExhausted` and demoted rather
// than spawning a Sentry error event for every embed call.
let app = Router::new().route(
"/v1/embeddings",
post(|| async {
(
StatusCode::BAD_REQUEST,
r#"{"success":false,"error":"Budget exceeded — add credits to continue"}"#,
)
}),
);
let url = start_mock(app).await;
let p = OpenAiEmbedding::new(&url, "k", "m", 1);

let err = p.embed(&["hi"]).await.unwrap_err();
let msg = err.to_string();
assert!(msg.contains("400"), "status: {msg}");
assert!(msg.contains("Budget exceeded"), "body: {msg}");
}

#[tokio::test]
async fn embed_missing_data_field() {
let app = Router::new().route(
Expand Down
Loading