mirror of
https://github.com/storytold/artcraft.git
synced 2026-11-03 08:57:54 +00:00
artcraft-mcp: include realized cost on generate_image responses
Lets the LLM truthfully answer 'what did that just cost?' without
remembering the pre-flight estimate. The backend has no per-job cost
endpoint or credit-transactions API; the only way to know the realized
cost is to diff the credit balance before and after.
generate_image::run now:
- Captures get_session_credits before submit.
- Runs generation + poll as before.
- Captures get_session_credits after success.
- Computes credits_charged = balance_before - balance_after.
- Attaches { credits_charged, credits_balance_after } to the response.
Best-effort: if either credit lookup fails the generation still
succeeds and the cost just isn't reported. Caveat noted in comments:
concurrent generations settling inside this window can over-count the
diff.
The tool's text content gains a 'Cost: N credits charged. Balance
after: M credits.' line when cost info is available — picked up by
both the user reading the chat and the LLM answering follow-up
questions about cost.
This commit is contained in:
1 parent
54246fe957
commit
eff94be8c0
2 files changed
+62
-4
No files matched your search
@@ -99,7 +99,7 @@ impl ArtcraftServer {
|
||||
}
|
||||
|
||||
#[tool(
|
||||
description = "Generate an image with Artcraft. Required: `prompt`. Optional: `model` (snake_case id — call list_image_models for valid values; defaults to nano_banana_pro), `aspect_ratio`, `resolution`, `quality`, `num_images`, `reference_image_urls` (https URLs for image-edit / img2img — only valid on models with image_refs_supported=true). Each parameter must satisfy the constraints reported by list_image_models for the chosen model. Blocks up to 90 seconds for completion. Returns the generated image inline plus its full-resolution CDN URL."
|
||||
description = "Generate an image with Artcraft. Required: `prompt`. Optional: `model` (snake_case id — call list_image_models for valid values; defaults to nano_banana_pro), `aspect_ratio`, `resolution`, `quality`, `num_images`, `reference_image_urls` (https URLs for image-edit / img2img — only valid on models with image_refs_supported=true). Each parameter must satisfy the constraints reported by list_image_models for the chosen model. Blocks up to 90 seconds for completion. Returns the generated images inline, plus their full-resolution CDN URLs and the realized cost (credits charged + remaining balance). The cost is derived by diffing the credit balance before and after generation."
|
||||
)]
|
||||
async fn generate_image(
|
||||
&self,
|
||||
@@ -157,7 +157,15 @@ async fn success_images_result(images: generate_image::GeneratedImages) -> CallT
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
};
|
||||
content.push(Content::text(urls_text));
|
||||
|
||||
let text = match &images.cost {
|
||||
Some(cost) => format!(
|
||||
"{}\n\nCost: {} credits charged. Balance after: {} credits.",
|
||||
urls_text, cost.credits_charged, cost.credits_balance_after
|
||||
),
|
||||
None => urls_text,
|
||||
};
|
||||
content.push(Content::text(text));
|
||||
|
||||
CallToolResult::success(content)
|
||||
}
|
||||
|
||||
@@ -7,12 +7,14 @@ use tokio::time::{sleep, Instant};
|
||||
|
||||
use artcraft_api_defs::omni_gen::cost_and_generate_requests::omni_gen_image_cost_and_generate_request::OmniGenImageCostAndGenerateRequest;
|
||||
use artcraft_client::credentials::storyteller_credential_set::StorytellerCredentialSet;
|
||||
use artcraft_client::endpoints::credits::get_session_credits::get_session_credits;
|
||||
use artcraft_client::endpoints::jobs::list_session_jobs::{list_session_jobs, States};
|
||||
use artcraft_client::endpoints::media_files::list_batch_generated_redux_media_files::list_batch_generated_redux_media_files;
|
||||
use artcraft_client::endpoints::media_files::upload_image_media_file_from_bytes::{
|
||||
upload_image_media_file_from_bytes, ImageType, UploadImageBytesArgs,
|
||||
};
|
||||
use artcraft_client::endpoints::omni_gen::generate::image::omni_gen_image::omni_gen_image_generate;
|
||||
use enums::common::payments_namespace::PaymentsNamespace;
|
||||
use artcraft_client::utils::api_host::ApiHost;
|
||||
use enums::common::generation::common_aspect_ratio::CommonAspectRatio;
|
||||
use enums::common::generation::common_image_model::CommonImageModel;
|
||||
@@ -38,6 +40,15 @@ pub struct GeneratedImage {
|
||||
|
||||
pub struct GeneratedImages {
|
||||
pub images: Vec<GeneratedImage>,
|
||||
/// None if either credit lookup failed; otherwise the realized cost
|
||||
/// of this generation derived by diffing the user's credit balance
|
||||
/// from just-before-submit to just-after-success.
|
||||
pub cost: Option<CostInfo>,
|
||||
}
|
||||
|
||||
pub struct CostInfo {
|
||||
pub credits_charged: u64,
|
||||
pub credits_balance_after: u64,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, schemars::JsonSchema)]
|
||||
@@ -113,6 +124,16 @@ pub async fn run(args: Args) -> Result<GeneratedImages, ToolError> {
|
||||
adjust_zoom: None,
|
||||
};
|
||||
|
||||
// Snapshot credits before submit so we can compute the realized cost
|
||||
// by diffing against the balance after success. Best-effort — if the
|
||||
// credits API hiccups we still complete the generation, we just don't
|
||||
// report a cost.
|
||||
let credits_before = get_session_credits(&api_host, Some(&creds), PaymentsNamespace::Artcraft)
|
||||
.await
|
||||
.map(|c| c.sum_total_credits)
|
||||
.map_err(|e| tracing::warn!("pre-submit credits lookup failed: {:?}", e))
|
||||
.ok();
|
||||
|
||||
tracing::info!("submitting omni_gen image request, model={:?}", model);
|
||||
|
||||
let submit = omni_gen_image_generate(&api_host, Some(&creds), request)
|
||||
@@ -125,7 +146,34 @@ pub async fn run(args: Args) -> Result<GeneratedImages, ToolError> {
|
||||
|
||||
tracing::info!("submitted; inference_job_token={:?}", submit.inference_job_token);
|
||||
|
||||
poll_for_image_url(&api_host, &creds, &submit.inference_job_token).await
|
||||
let mut result = poll_for_image_url(&api_host, &creds, &submit.inference_job_token).await?;
|
||||
|
||||
// Compute realized cost. If concurrent generations on other tools
|
||||
// settle in this window the diff can over-count, so we annotate
|
||||
// the value in the log; the LLM gets the number either way.
|
||||
let credits_after = get_session_credits(&api_host, Some(&creds), PaymentsNamespace::Artcraft)
|
||||
.await
|
||||
.map(|c| c.sum_total_credits)
|
||||
.map_err(|e| {
|
||||
tracing::warn!("post-success credits lookup failed: {:?}", e);
|
||||
})
|
||||
.ok();
|
||||
|
||||
if let (Some(before), Some(after)) = (credits_before, credits_after) {
|
||||
let charged = before.saturating_sub(after);
|
||||
tracing::info!(
|
||||
"realized cost: {} credits (balance {} -> {})",
|
||||
charged,
|
||||
before,
|
||||
after
|
||||
);
|
||||
result.cost = Some(CostInfo {
|
||||
credits_charged: charged,
|
||||
credits_balance_after: after,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
async fn poll_for_image_url(
|
||||
@@ -194,9 +242,10 @@ async fn poll_for_image_url(
|
||||
.maybe_thumbnail_template
|
||||
.clone(),
|
||||
}],
|
||||
cost: None,
|
||||
});
|
||||
}
|
||||
return Ok(GeneratedImages { images });
|
||||
return Ok(GeneratedImages { images, cost: None });
|
||||
}
|
||||
|
||||
return Ok(GeneratedImages {
|
||||
@@ -204,6 +253,7 @@ async fn poll_for_image_url(
|
||||
cdn_url: result.media_links.cdn_url.to_string(),
|
||||
maybe_thumbnail_template: result.media_links.maybe_thumbnail_template.clone(),
|
||||
}],
|
||||
cost: None,
|
||||
});
|
||||
}
|
||||
JobStatusPlus::CompleteFailure | JobStatusPlus::Dead => {
|
||||
|
||||
Reference in new issue
Block a user