Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
82 commits
Select commit Hold shift + click to select a range
4c5da84
chore(deps): add image-rs dependency to tinyinference-image
senamakel Sep 23, 2026
ac958c6
fix(error): handle missing image dimensions in error conversion
senamakel Sep 23, 2026
101811c
fix(transport): handle missing content-length header
senamakel Sep 23, 2026
43c6f75
fix(reference): handle empty image reference in inference
senamakel Sep 23, 2026
747e97e
fix(media): handle missing image dimensions in media type detection
senamakel Sep 23, 2026
b5e14af
feat(capabilities): add support for image generation capabilities
senamakel Sep 23, 2026
a13be74
fix(image): add missing serde attributes to image types
senamakel Sep 23, 2026
98ed713
feat(openrouter): add OpenRouter image inference support
senamakel Sep 23, 2026
0b51f52
fix(image): restore image decoding support
senamakel Sep 23, 2026
f6b60c4
chore: add mock inference implementation
senamakel Sep 23, 2026
ef427a9
chore: files changed crates/tinyinference-image/src/openrouter_test.rs
senamakel Sep 23, 2026
94c9b91
fix(reference_test): restore missing test assertions
senamakel Sep 23, 2026
effcb48
chore(deps): add tinyinference-image package to lockfile
senamakel Sep 23, 2026
a6db20f
chore: reformat long method chains and nested expressions for readabi…
senamakel Sep 23, 2026
7da3e34
fix(docs): use mod@ disambiguator for intra-doc links to reference mo…
senamakel Sep 23, 2026
236bdc0
chore(tinyinference-video): add missing dependency for video processing
senamakel Sep 23, 2026
3157f44
fix(error): handle missing video file path in error conversion
senamakel Sep 23, 2026
d20143d
fix(types): correct video type field name from "type" to "video_type
senamakel Sep 23, 2026
558f418
chore: add video inference crate
senamakel Sep 23, 2026
9ede102
fix(openrouter): handle missing video URL in response
senamakel Sep 23, 2026
3c67097
chore: add mock video inference implementation
senamakel Sep 23, 2026
ae51156
fix(test): update test to reflect new inference behavior
senamakel Sep 23, 2026
eb2e484
fix(test): add openrouter test file
senamakel Sep 23, 2026
c234cec
chore(deps): add tinyinference-video package to lockfile
senamakel Sep 23, 2026
75e3a9c
chore: format code with rustfmt
senamakel Sep 23, 2026
05adc16
fix(video): box large error variant to reduce enum size
senamakel Sep 23, 2026
3dac06a
feat(examples): add live OpenRouter image example
senamakel Sep 23, 2026
5968ea8
feat(video): add live OpenRouter video example
senamakel Sep 23, 2026
5982c05
chore(examples): reformat long method chains and function calls
senamakel Sep 23, 2026
5c5e6c9
docs: document new image and video generation crates
senamakel Sep 23, 2026
0e3081f
feat(transport): unwrap proxied backend success envelope
senamakel Sep 23, 2026
0ed6625
fix(transport): unwrap envelope error message from nested JSON
senamakel Sep 23, 2026
f6ac355
feat(examples): include creation timestamp in persisted image filenames
senamakel Sep 24, 2026
c126ef9
docs(env): document OPENROUTER_API_KEY for the live media examples
senamakel Sep 24, 2026
d634551
fix(transport): handle missing content-length header in image response
senamakel Sep 24, 2026
fab7ca2
fix(transport): handle missing content-length header in response
senamakel Sep 24, 2026
915394c
fix(transport): handle missing content-type header in response
senamakel Sep 24, 2026
38a3878
fix(transport): handle missing content-length header for chunked resp…
senamakel Sep 24, 2026
4859408
fix(transport): handle missing content-length header in response
senamakel Sep 24, 2026
027db27
feat(openrouter): add OpenRouter video inference support
senamakel Sep 24, 2026
e44b052
fix(tyinference-video): correct video inference output dimensions
senamakel Sep 24, 2026
8e5725c
refactor(tinyinference-video): remove timeout logic from wait_for_job
senamakel Sep 24, 2026
a982154
fix(error): handle missing video codec in error conversion
senamakel Sep 24, 2026
f822f6d
fix(error): handle missing image dimensions in error conversion
senamakel Sep 24, 2026
ab7ed87
fix(media): handle missing image dimensions in metadata
senamakel Sep 24, 2026
2cc0327
chore: files changed crates/tinyinference-image/src/openrouter.rs
senamakel Sep 24, 2026
fc5d889
fix(openrouter): handle missing image URL in response
senamakel Sep 24, 2026
57a1fae
fix(openrouter): handle missing video URL in response
senamakel Sep 24, 2026
1d8b436
fix(video): handle missing video file path in error conversion
senamakel Sep 24, 2026
ec0ca22
fix(tyinference-video): correct video inference output dimensions
senamakel Sep 24, 2026
f61d29d
fix(tyinference-video): correct video inference output dimensions
senamakel Sep 24, 2026
e6830a5
fix(tinyinference-video): reformat download_all call arguments
senamakel Sep 24, 2026
7d080fa
fix(scope): correct video inference output dimension calculation
senamakel Sep 24, 2026
42fdff9
fix(video): handle empty frame list in inference
senamakel Sep 24, 2026
3cc3f92
chore(tinyinference-video): add missing dependency
senamakel Sep 24, 2026
479a407
chore(tinyinference-video): remove unused test file
senamakel Sep 24, 2026
482fe5f
fix(test): update test to match new inference behavior
senamakel Sep 24, 2026
76db6c1
chore(deps): add tokio-test and tokio-stream dependencies
senamakel Sep 24, 2026
efe152d
fix(reference): handle missing image reference gracefully
senamakel Sep 24, 2026
b4c1793
fix(reference): handle missing image metadata gracefully
senamakel Sep 24, 2026
5f7bcbf
fix(reference): handle empty image list in reference resolution
senamakel Sep 24, 2026
eba85b9
fix(reference): handle missing image dimensions in metadata
senamakel Sep 24, 2026
8e65e09
feat(reference): add support for image reference resolution
senamakel Sep 24, 2026
79dacfa
fix(reference): handle missing image metadata gracefully
senamakel Sep 24, 2026
f58c1d1
fix(reference): inline debug formatting for MediaReference
senamakel Sep 24, 2026
5b238ea
fix(transport): handle missing content-length header in response
senamakel Sep 24, 2026
49a1503
fix(transport): handle missing content-length header in image response
senamakel Sep 24, 2026
4e93582
fix(scope): correct video inference output format
senamakel Sep 24, 2026
050d921
feat(capabilities): add support for image generation capabilities
senamakel Sep 24, 2026
ac5b640
feat(types): add support for image dimensions in type definitions
senamakel Sep 24, 2026
ac5bee9
fix(types): correct image type validation for empty inputs
senamakel Sep 24, 2026
09a73e0
fix(transport): handle connection reset errors during inference
senamakel Sep 24, 2026
fb1551c
fix(reference): handle missing image dimensions in metadata
senamakel Sep 24, 2026
61531a5
fix(reference): handle missing image dimensions in metadata
senamakel Sep 24, 2026
66c4f75
fix(scope): correct video inference output handling
senamakel Sep 24, 2026
0c9e3fe
fix(scope): correct video inference output handling
senamakel Sep 24, 2026
6393465
fix(scope): correct video inference output handling
senamakel Sep 24, 2026
91f6680
fix(video): reformat fallback download call for readability
senamakel Sep 24, 2026
a8c7cb2
fix(media): paused-clock deadlines, bounded image JSON, redacted base…
senamakel Sep 24, 2026
d0d329c
fix(media): inline typed local refs, precheck data URL size, one boun…
senamakel Sep 24, 2026
24c7058
feat(video): handle sparse output indices from OpenRouter
senamakel Sep 24, 2026
6081aef
test(openrouter): add tests for omitted enum capability and sparse ou…
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
5 changes: 5 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,8 @@
# Example of a credential a live/network-gated test would need. Tests that
# require one must skip cleanly when it is unset.
# EXAMPLE_API_KEY=replace-me

# OpenRouter API key for the live media examples
# (`live_openrouter_image`, `live_openrouter_video`). They skip when unset.
# Live runs are billed to this key.
# OPENROUTER_API_KEY=sk-or-replace-me
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,20 @@
# Changelog

## Unreleased

### Added

- `tinyinference-image`: the `ImageGenerator` trait, `OpenRouterImageGenerator`
(`POST /images`), `MockImageGenerator`, media-reference standards
(URL, `data:` URL, bytes, local path → OpenRouter content parts), aspect-ratio,
resolution and size normalization, per-model capability pre-flight checks, and
a billing-aware OpenRouter media transport usable directly or through a
proxying backend.
- `tinyinference-video`: the `VideoGenerator` trait, `OpenRouterVideoGenerator`
(`POST /videos`, `GET /videos/{id}`, `GET /videos/{id}/content`),
`wait_for_job` (resume by job id), and `MockVideoGenerator`. A `completed`
job with no outputs keeps polling instead of failing.

## 0.3.0

### Breaking changes
Expand Down
55 changes: 55 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

25 changes: 23 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,8 +50,11 @@ can depend on `tinyinference-llm` for language models,
`tinyinference-embeddings` for vector generation and retrieval,
`tinyinference-local` for local runtimes and installers,
`tinyinference-providers` for provider authentication and routing primitives,
`tinyinference-voice` for speech inference and streaming-audio mechanics, and
`tinyinference-core` only for shared infrastructure.
`tinyinference-voice` for speech inference and streaming-audio mechanics,
`tinyinference-image` for image generation and the shared media-reference
standards and OpenRouter media transport,
`tinyinference-video` for asynchronous video generation (submit, poll,
download, resume), and `tinyinference-core` only for shared infrastructure.

## Layout

Expand Down Expand Up @@ -80,8 +83,26 @@ crates/tinyinference-providers/
└── src/ OAuth/PKCE flows and provider error classification
crates/tinyinference-voice/
└── src/ hosted STT, Piper TTS, cleanup, and PCM streaming helpers
crates/tinyinference-image/
└── src/ ImageGenerator, media references and output-shape
normalization, OpenRouter media transport, capabilities
crates/tinyinference-video/
└── src/ VideoGenerator, submit/poll/download job loop, resume by id
```

### Media generation

`tinyinference-image` and `tinyinference-video` speak OpenRouter's media wire
format (`POST /images`, `POST /videos`, `GET /videos/{id}`,
`GET /videos/{id}/content`). The same generators run against OpenRouter
directly (`MediaAuth::ApiKey`) or against a host backend that proxies those
routes verbatim (`MediaAuth::Bearer` with `MediaTransport::with_base_url`).
A generator returns delivered media or an error — never an empty success —
and every error after a billed submit names the job and says not to resubmit.
Live smoke tests: `cargo run -p tinyinference-image --example
live_openrouter_image` and `cargo run -p tinyinference-video --example
live_openrouter_video` (skip without `OPENROUTER_API_KEY`).

## Development

```sh
Expand Down
32 changes: 32 additions & 0 deletions crates/tinyinference-image/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[package]
name = "tinyinference-image"
Comment thread
senamakel marked this conversation as resolved.
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
repository.workspace = true
description = "Provider-neutral image generation, media references, and OpenRouter transport for Rust."
documentation = "https://docs.rs/tinyinference-image"
readme = "../../README.md"
keywords = ["image-generation", "inference", "openrouter", "media"]
categories = ["api-bindings", "asynchronous", "multimedia::images"]

[dependencies]
async-trait = { workspace = true }
base64 = "0.23"
bytes = { workspace = true }
reqwest = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
tinyinference-core = { version = "0.3.0", path = "../tinyinference-core" }
tokio = { workspace = true, features = ["fs"] }
tracing = { workspace = true }

[dev-dependencies]
axum = { workspace = true }
tempfile = { workspace = true }
tokio = { workspace = true, features = ["fs", "net"] }

[lints]
workspace = true
95 changes: 95 additions & 0 deletions crates/tinyinference-image/examples/live_openrouter_image.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//! Live smoke test: generate an image through OpenRouter and save it.
//!
//! Network- and credential-gated. Reads `OPENROUTER_API_KEY` from the
//! environment, falling back to the workspace `.env` file; exits cleanly
//! (status 0, "skipped") when neither provides one.
//!
//! ```sh
//! cargo run -p tinyinference-image --example live_openrouter_image
//! # image-to-image: pass a reference image (path or URL)
//! LIVE_REFERENCE=path/to/ref.png cargo run -p tinyinference-image --example live_openrouter_image
//! # optional overrides
//! LIVE_IMAGE_MODEL=google/gemini-3.1-flash-lite-image LIVE_PROMPT="…" cargo run …
//! ```
//!
//! Output lands in `target/live-media/`.

use std::path::PathBuf;
use std::time::Instant;

use tinyinference_image::{
ImageGenerator, ImageRequest, MediaAuth, MediaReference, OpenRouterImageGenerator,
};

fn api_key() -> Option<String> {
if let Ok(key) = std::env::var("OPENROUTER_API_KEY")
&& !key.trim().is_empty()
{
return Some(key.trim().to_owned());
}
let env_file = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../.env");
std::fs::read_to_string(env_file)
.ok()?
.lines()
.find_map(|line| {
let value = line.trim().strip_prefix("OPENROUTER_API_KEY=")?;
let value = value.trim().trim_matches('"').trim_matches('\'');
(!value.is_empty()).then(|| value.to_owned())
})
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let Some(key) = api_key() else {
println!("skipped: OPENROUTER_API_KEY is not set (env or workspace .env)");
return Ok(());
};
let generator = OpenRouterImageGenerator::new(MediaAuth::ApiKey(key));
let model =
std::env::var("LIVE_IMAGE_MODEL").unwrap_or_else(|_| generator.default_model().to_owned());
let prompt = std::env::var("LIVE_PROMPT").unwrap_or_else(|_| {
"A four-panel anime comic of two cheerful engineers shaking hands in front of a glowing \
server, bright colors, thick ink outlines"
.to_owned()
});

let mut request = ImageRequest::new(prompt)
.with_model(&model)
.with_aspect_ratio("landscape")
.with_seed(42);
if let Ok(reference) = std::env::var("LIVE_REFERENCE") {
println!("image-to-image with reference {reference}");
request = request.with_reference(MediaReference::parse(&reference));
}

let started = Instant::now();
let response = generator.generate(request).await?;
let out_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/live-media");
for (index, image) in response.images.iter().enumerate() {
let path = image
.persist(
&out_dir,
&format!(
"image-{}-{}-{index}",
model.replace('/', "_"),
response.created.unwrap_or_default()
),
"png",
)
.await?;
println!(
"saved {} ({} bytes, {})",
path.display(),
image.data.len(),
image.media_type
);
}
println!(
"model={} images={} cost_usd={:?} elapsed={:.1}s",
response.model,
response.images.len(),
response.cost_usd,
started.elapsed().as_secs_f64()
);
Ok(())
}
Loading
Loading