Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
757b29e
feat(openrouter): add media support to agent integrations
senamakel Sep 23, 2026
ffcb043
chore: update lib.rs formatting
senamakel Sep 23, 2026
d8b8a93
fix(agent_integrations): handle missing agent gracefully on startup
senamakel Sep 23, 2026
d8ce208
chore(agent_integrations): remove unused imports
senamakel Sep 23, 2026
f1f54c1
chore(agent_integrations): add module for agent integrations
senamakel Sep 23, 2026
922708e
fix(openrouter): handle missing model field in API response
senamakel Sep 23, 2026
8718020
fix(openrouter): add missing OpenRouter integration
senamakel Sep 23, 2026
d878626
fix(agent_integration_types): add missing fields for agent configuration
senamakel Sep 23, 2026
a19e38a
test(agent): add integration test for module layout
senamakel Sep 23, 2026
ef4cf8f
test(agent): add integration test for module layout
senamakel Sep 23, 2026
74b98e0
fix(media_generation): handle empty response from media generation API
senamakel Sep 23, 2026
348c2c3
test: allow deprecated items in media generation tests
senamakel Sep 23, 2026
7ec6fca
fix(tests): update OpenRouter test to match new API response format
senamakel Sep 23, 2026
a116bf2
fix(tests): correct OpenRouter test assertion for API response format
senamakel Sep 23, 2026
0e3e5a5
feat(api): add PUT route for orchestrator endpoint
senamakel Sep 23, 2026
4dac8a4
chore: format OpenRouter media code and tests
senamakel Sep 23, 2026
04f7a98
chore: update lib.rs
senamakel Sep 23, 2026
e2cd9a1
fix(tests): add openapi sync test
senamakel Sep 23, 2026
2a8c512
docs(api-surface): add missing API documentation for user endpoints
senamakel Sep 23, 2026
0b96211
chore(api): update tinyhumans backend configuration
senamakel Sep 24, 2026
7a4766f
docs(api-surface): add missing API surface documentation
senamakel Sep 24, 2026
351b3b0
feat(api): add media generation endpoint for agent integrations
senamakel Sep 24, 2026
3e93c7e
fix(openrouter): handle missing media field in API response
senamakel Sep 24, 2026
1cdf8b6
fix(openrouter): handle media content in agent integrations
senamakel Sep 24, 2026
ecf22d0
fix(openrouter): handle missing media field in API response
senamakel Sep 24, 2026
edc8484
fix(openrouter): handle missing media field in API response
senamakel Sep 24, 2026
cb0aea2
fix(public_routes): correct route generation for nested modules
senamakel Sep 24, 2026
96c9f13
fix(parser): handle missing trailing newline in input
senamakel Sep 24, 2026
2bd0540
chore(api): update route counts and expose additional public routes
senamakel Sep 24, 2026
667cbbb
chore(scripts): remove sync-openapi script
senamakel Sep 24, 2026
4c26edd
chore(api): update excluded admin operation count and add unexposed r…
senamakel Sep 24, 2026
a541277
fix(parser): handle missing trailing newline in input
senamakel Sep 24, 2026
44948c4
fix(tests): correct test assertion for OpenAPI sync response
senamakel Sep 24, 2026
9c806f5
chore(api): update backend spec and regenerate public routes
senamakel Sep 24, 2026
34866d9
fix(test): update expected unexposed route count to 59
senamakel Sep 24, 2026
4797c26
fix(tests): update expected admin operation count in sync test
senamakel Sep 24, 2026
d68ad56
fix(openrouter): handle media content in agent integrations
senamakel Sep 24, 2026
1180aef
fix(tests): update OpenRouter test to use correct API endpoint
senamakel Sep 24, 2026
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: 3 additions & 3 deletions api/tinyhumans.backend.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@
"url": "https://api.tinyhumans.ai/swagger.json",
"title": "TinyHumans API",
"version": "1.0.0",
"pathCount": 237,
"totalOperationCount": 262,
"pathCount": 238,
"totalOperationCount": 263,
"operationCount": 208,
"supplementalOperationCount": 13,
"excludedAdminOperationCount": 46,
"excludedAdminOperationCount": 47,
"excludedWebhookOperationCount": 12,
"servers": [
"https://api.tinyhumans.ai/",
Expand Down
38 changes: 37 additions & 1 deletion docs/api-surface.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

The SDK surface is grounded in the deployed Swagger/OpenAPI contract at
<https://api.tinyhumans.ai/swagger.json>. The spec reports TinyHumans API
`1.0.0` with 161 paths and 182 operations. The Rust SDK exposes one typed
`1.0.0` with 182 paths and 196 operations. The Rust SDK exposes one typed
method per public operation — **197 operations across the 21 namespaces
below**.
The remaining 32 administrative and 12 webhook-receiver operations are
Expand Down Expand Up @@ -67,3 +67,39 @@ Most JSON responses use the hosted-backend envelope:

SDK request helpers unwrap this envelope by default. The raw helper can return the
full response body when callers need status metadata or non-standard payloads.

## OpenRouter media generation

`agent_integrations::openrouter` exposes the direct OpenRouter proxy under
`/agent-integrations/openrouter/*`, including image (`POST /images`) and video
(`POST /videos`, `GET /videos/{jobId}`, `GET /videos/{jobId}/content`)
generation with untyped `impl Serialize` request bodies (OpenRouter's own API
is the contract for this surface).

`agent_integrations::openrouter_media` adds a fully typed alternative for the
media routes only — `OpenRouterImageRequest`/`OpenRouterImageResponse`,
`OpenRouterVideoRequest`, `ContentPartImage`, `FrameImage` — with an `extra`
flattened map on each request struct so an upstream field this module does not
yet model is still forwarded. Both modules call the same routes; pick whichever
fits the caller (`openrouter_images`/`openrouter_videos`/
`openrouter_image_models`/`openrouter_video_models` for typed,
`openrouter_create_image`/`openrouter_create_video`/
`list_openrouter_image_models`/`list_openrouter_video_models` for
passthrough). `get_openrouter_video`, `openrouter_video_content`, and the typed
module's `openrouter_video_content_with_type` (which also surfaces the
upstream `content-type`, since the plain byte helper drops response headers)
are shared by both.

`OpenRouterMediaModel` (returned by both the typed and untyped model listings)
carries OpenRouter's capability descriptors verbatim when the backend's
catalog published them: `supported_parameters`/`architecture` for image
models, and `supported_resolutions`/`supported_aspect_ratios`/
`supported_durations`/`supported_sizes`/`supported_frame_images`/
`generate_audio`/`seed`/`allowed_passthrough_parameters` for video models —
so a caller can validate a request against a model's real capabilities before
submitting it.

The older GMI-backed `agent_integrations::media_generation` module
(`/agent-integrations/media-generation/*`) is deprecated in favor of the
OpenRouter surface above; its methods are `#[deprecated]` but remain
functional.
Comment thread
senamakel marked this conversation as resolved.
1 change: 1 addition & 0 deletions scripts/sync-openapi.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const RETAINED_UNEXPOSED_ROUTES = [
// list exists to prevent. Declared here so both paths agree.
["POST", "/opencompany/instances/{slug}/inference-key"],
["DELETE", "/opencompany/instances/{slug}/inference-key"],
["PUT", "/opencompany/instances/{slug}/orchestrator"],
["POST", "/opencompany/instances/{slug}/usage"],
// Guild (teeny Discord service) callbacks, gated by GUILD_SERVICE_TOKEN.
["POST", "/internal/discord/link"],
Expand Down
1 change: 1 addition & 0 deletions src/api/agent_integration_types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ pub use super::agent_integrations::financial_apis::*;
pub use super::agent_integrations::google_places::*;
pub use super::agent_integrations::history_rewards::*;
pub use super::agent_integrations::media_generation::*;
pub use super::agent_integrations::openrouter_media::*;
pub use super::agent_integrations::parallel::*;
pub use super::agent_integrations::pricing::*;
pub use super::agent_integrations::recall_calendar::*;
Expand Down
39 changes: 39 additions & 0 deletions src/api/agent_integrations/media_generation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@ pub struct MediaResponse {
pub error: Option<String>,
}

/// Legacy flat model shape. No current backend deployment sends this — kept
/// only so an old snapshot from before the `curated`/`upstream` split (or a
/// non-conforming custom entry) still deserializes to something.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct MediaModel {
Expand All @@ -87,14 +90,43 @@ pub struct MediaModel {
pub capabilities: Value,
}

/// One entry in the curated GMI media catalog
/// (`MediaModelInfo` on the backend).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
pub struct CuratedMediaModel {
Comment thread
senamakel marked this conversation as resolved.
pub id: String,
#[serde(default)]
pub modality: String,
#[serde(default)]
pub kinds: Vec<String>,
#[serde(default)]
pub base_cost_usd: f64,
Comment thread
senamakel marked this conversation as resolved.
#[serde(default)]
pub description: String,
}

/// `GET /agent-integrations/media-generation/models` response
/// (`MediaListModelsControllerResponse` on the backend): the curated catalog,
/// plus GMI's live model ids when `includeUpstream=true` was requested and
/// available.
///
/// `models` is kept for backward compatibility with the pre-`curated`/
/// `upstream` flat shape (see [`MediaModel`]) — the current backend never
/// populates it, so it is always empty on a live response.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct MediaModelsResponse {
#[serde(default)]
pub curated: Vec<CuratedMediaModel>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub upstream: Option<Vec<String>>,
#[serde(default)]
pub models: Vec<MediaModel>,
}

impl AgentIntegrationsApi<'_> {
/// Generate or edit an image via GMI (Seedream / SeedEdit).
#[deprecated(note = "use AgentIntegrationsApi::openrouter_images")]
Comment thread
senamakel marked this conversation as resolved.
pub async fn media_generation_images(
&self,
request: &impl Serialize,
Expand All @@ -104,6 +136,9 @@ impl AgentIntegrationsApi<'_> {
}

/// List curated media-generation models.
#[deprecated(
note = "use AgentIntegrationsApi::openrouter_image_models / openrouter_video_models"
)]
pub async fn list_media_generation_models(
&self,
query: &[QueryParam],
Expand All @@ -120,6 +155,9 @@ impl AgentIntegrationsApi<'_> {
}

/// Poll a media-generation request.
#[deprecated(
note = "use AgentIntegrationsApi::get_openrouter_video / openrouter_video_content"
)]
pub async fn get_media_generation_request(
&self,
request_id: &str,
Expand All @@ -132,6 +170,7 @@ impl AgentIntegrationsApi<'_> {
}

/// Generate a video via GMI (Seedance / Veo).
#[deprecated(note = "use AgentIntegrationsApi::openrouter_videos")]
pub async fn media_generation_videos(
&self,
request: &impl Serialize,
Expand Down
14 changes: 14 additions & 0 deletions src/api/agent_integrations/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ pub mod google_places;
pub mod history_rewards;
pub mod media_generation;
pub mod openrouter;
pub mod openrouter_media;
Comment thread
senamakel marked this conversation as resolved.
pub mod parallel;
pub mod pricing;
pub mod recall_calendar;
Expand All @@ -37,6 +38,7 @@ pub use google_places::*;
pub use history_rewards::*;
pub use media_generation::*;
pub use openrouter::*;
pub use openrouter_media::*;
pub use parallel::*;
pub use pricing::*;
pub use recall_calendar::*;
Expand Down Expand Up @@ -109,4 +111,16 @@ impl<'a> AgentIntegrationsApi<'a> {
) -> Result<Vec<u8>, Error> {
self.http.send_bytes_query(method, path, query).await
}

/// [`Self::bytes_query`], but also returns the upstream `content-type`.
async fn bytes_query_with_type(
&self,
method: Method,
path: &str,
query: &[QueryParam],
) -> Result<(Vec<u8>, Option<String>), Error> {
self.http
.send_bytes_query_with_content_type(method, path, query)
.await
}
}
66 changes: 65 additions & 1 deletion src/api/agent_integrations/openrouter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,19 +74,66 @@ pub struct OpenRouterModelsResponse {
pub offset: u64,
}

/// Input/output modality lists for an image model (`architecture` on the
/// upstream image catalog).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct OpenRouterImageArchitecture {
#[serde(default)]
pub input_modalities: Vec<String>,
#[serde(default)]
pub output_modalities: Vec<String>,
}

/// One image or video model.
///
/// Media models are not token-priced, so this carries no per-1M block. Video
/// models publish a flat `price_per_generation`; image models publish none at
/// all, because an image is billed at the exact cost the generation response
/// reports.
///
/// The capability fields below are passed through from the cached upstream
/// catalog entry when the backend's listing carried them, so a caller can
/// validate a request pre-flight against this model's actual capabilities
/// instead of guessing. All are `None`/empty when upstream did not publish
/// them for this model.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct OpenRouterMediaModel {
pub id: String,
#[serde(default)]
pub display_name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub price_per_generation: Option<f64>,
/// Image models only — a typed descriptor map, e.g. `resolution`/`seed`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub supported_parameters: Option<serde_json::Map<String, serde_json::Value>>,
/// Image models only.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub architecture: Option<OpenRouterImageArchitecture>,
/// Video models only.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub supported_resolutions: Option<Vec<String>>,
/// Video models only.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub supported_aspect_ratios: Option<Vec<String>>,
/// Video models only.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub supported_durations: Option<Vec<u32>>,
/// Video models only.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub supported_sizes: Option<Vec<String>>,
/// Video models only.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub supported_frame_images: Option<Vec<String>>,
/// Video models only.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub generate_audio: Option<bool>,
/// Video models only — whether the model supports deterministic
/// generation via a `seed` parameter.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub seed: Option<bool>,
/// Video models only.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub allowed_passthrough_parameters: Option<Vec<String>>,
}

#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
Expand All @@ -103,7 +150,16 @@ pub struct OpenRouterMediaModelsResponse {
pub offset: u64,
}

/// An accepted video generation job. Poll `id` until `status` is terminal.
/// Cost block on a completed video job (`usage.cost` on the polled response).
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct OpenRouterVideoUsage {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub cost: Option<f64>,
}

/// An accepted video generation job, and its polled status. Poll `id` (via
/// [`AgentIntegrationsApi::get_openrouter_video`]) until `status` is
/// terminal: `unsigned_urls` and `usage` are only populated once it is.
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
pub struct OpenRouterVideoJob {
#[serde(default)]
Expand All @@ -116,6 +172,14 @@ pub struct OpenRouterVideoJob {
pub polling_url: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
/// Upstream-hosted asset URLs, present once `status` is `"completed"`.
/// Prefer `openrouter_video_content`/`openrouter_video_content_with_type`
/// (ownership-checked, streamed through the backend) over fetching these
/// directly.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub unsigned_urls: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub usage: Option<OpenRouterVideoUsage>,
}

impl AgentIntegrationsApi<'_> {
Expand Down
Loading
Loading