Skip to content
Open
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
9 changes: 9 additions & 0 deletions crates/tinymemory-api/src/host/cloud_providers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,12 @@ pub const BUILTIN_CLOUD_PROVIDERS: &[BuiltinCloudProvider] = &[
endpoint: "https://openrouter.ai/api/v1",
auth_style: AuthStyle::Bearer,
},
BuiltinCloudProvider {
slug: "inworld",
label: "Inworld",
endpoint: "https://api.inworld.ai/v1",
auth_style: AuthStyle::Basic,
},
BuiltinCloudProvider {
slug: "orcarouter",
label: "OrcaRouter",
Expand Down Expand Up @@ -326,6 +332,8 @@ pub enum AuthStyle {
/// OpenAI-compatible: `Authorization: Bearer <key>`
#[default]
Bearer,
/// HTTP Basic: `Authorization: Basic <credential>`
Basic,
/// Anthropic: `x-api-key: <key>` + `anthropic-version: 2023-06-01`
Anthropic,
/// OpenHuman session JWT (injected by the backend provider, not stored here).
Expand All @@ -338,6 +346,7 @@ impl AuthStyle {
pub fn as_str(&self) -> &'static str {
match self {
Self::Bearer => "bearer",
Self::Basic => "basic",
Self::Anthropic => "anthropic",
Self::OpenhumanJwt => "openhuman_jwt",
Self::None => "none",
Expand Down
33 changes: 25 additions & 8 deletions crates/tinymemory-api/src/host/cloud_providers_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,12 @@ fn builtin_cloud_provider_defaults_cover_phase_one_presets() {
"https://api-inference.modelscope.cn/v1",
AuthStyle::Bearer,
),
(
"inworld",
"Inworld",
"https://api.inworld.ai/v1",
AuthStyle::Basic,
),
] {
let mut entry = CloudProviderCreds {
id: format!("p_{slug}"),
Expand Down Expand Up @@ -93,7 +99,7 @@ fn builtin_cloud_provider_slugs_are_unique() {

#[test]
fn is_builtin_cloud_slug_matches_presets_only() {
for slug in ["openai", "deepseek", "groq", "mistral"] {
for slug in ["openai", "deepseek", "groq", "mistral", "inworld"] {
assert!(is_builtin_cloud_slug(slug), "{slug} is a built-in preset");
}
for slug in ["my-proxy", "custom-openai", "totally-unknown", ""] {
Expand Down Expand Up @@ -167,6 +173,7 @@ fn host_is_builtin_cloud_provider_recognises_inference_hosts() {
"api.openai.com",
"api.anthropic.com",
"api.groq.com",
"api.inworld.ai",
"generativelanguage.googleapis.com",
"API.OPENAI.COM", // case-insensitive
] {
Expand Down Expand Up @@ -211,6 +218,7 @@ fn nvidia_host_is_chat_completions_only_regardless_of_slug() {
"https://api.deepseek.com/v1",
"https://api.groq.com/openai/v1",
"https://api.mistral.ai/v1",
"https://api.inworld.ai/v1",
] {
assert!(
endpoint_host_is_chat_completions_only(endpoint),
Expand Down Expand Up @@ -248,8 +256,8 @@ fn host_gate_agrees_with_slug_capability_for_every_builtin() {
for provider in BUILTIN_CLOUD_PROVIDERS {
// OpenhumanJwt / Anthropic presets never route through the
// OpenAI-compatible Responses fallback; the gate only matters for
// the Bearer OpenAI-compatible hosts.
if provider.auth_style != AuthStyle::Bearer {
// Bearer and Basic OpenAI-compatible hosts.
if !matches!(provider.auth_style, AuthStyle::Bearer | AuthStyle::Basic) {
continue;
}
let host_chat_only = endpoint_host_is_chat_completions_only(provider.endpoint);
Expand All @@ -265,13 +273,22 @@ fn host_gate_agrees_with_slug_capability_for_every_builtin() {
#[test]
fn auth_styles_and_legacy_provider_types_expose_stable_wire_properties() {
let styles = [
(AuthStyle::Bearer, "bearer"),
(AuthStyle::Anthropic, "anthropic"),
(AuthStyle::OpenhumanJwt, "openhuman_jwt"),
(AuthStyle::None, "none"),
(AuthStyle::Bearer, "bearer", "bearer"),
(AuthStyle::Basic, "basic", "basic"),
(AuthStyle::Anthropic, "anthropic", "anthropic"),
// Preserve the historical serde value while `as_str` retains the
// underscore used by host configuration code.
(AuthStyle::OpenhumanJwt, "openhuman_jwt", "openhumanjwt"),
(AuthStyle::None, "none", "none"),
];
for (style, expected) in styles {
for (style, expected, wire_value) in styles {
assert_eq!(style.as_str(), expected);
let json = serde_json::to_string(&style).expect("auth style serializes");
assert_eq!(json, format!("\"{wire_value}\""));
assert_eq!(
serde_json::from_str::<AuthStyle>(&json).expect("auth style deserializes"),
style
);
}

let types = [
Expand Down
24 changes: 24 additions & 0 deletions docs/plans/inworld-cloud-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Inworld cloud provider metadata plan

Specification: [Inworld cloud provider metadata](../specs/inworld-cloud-provider.md)

## Goal

Add the smallest host contract required for an Inworld LLM provider without
adding request transport or credential storage to TinyMemory.

## Tasks

1. Extend cloud-provider tests for the `basic` wire value, Inworld preset,
legacy migration, host recognition, and chat-only classification.
2. Add `AuthStyle::Basic` and the Inworld built-in catalog entry in
`crates/tinymemory-api/src/host/cloud_providers.rs`.
3. Run formatting, Clippy, build, and test contract commands.

## Completion checklist

- [x] Focused cloud-provider tests pass.
- [x] `cargo fmt --all -- --check` passes.
- [x] `cargo clippy --all-targets --all-features -- -D warnings` passes.
- [x] `cargo build --all-targets --all-features` passes.
- [x] `cargo test --all-features` passes.
61 changes: 61 additions & 0 deletions docs/specs/inworld-cloud-provider.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Inworld cloud provider metadata

**Status:** Accepted

**Owner:** TinyMemory maintainers

## Problem

Hosts that consume TinyMemory's cloud-provider contract cannot represent an
OpenAI-compatible provider that authenticates with HTTP Basic credentials.
They also need one canonical Inworld preset so configuration, migration, and
backend-host safety logic agree on its endpoint and authentication style.

## Goals

- Represent HTTP Basic authentication as a stable `basic` wire value.
- Publish Inworld as a built-in OpenAI-compatible cloud provider.
- Classify Inworld's API host as an inference host and its LLM endpoint as
chat-completions-only.
- Preserve existing provider metadata and legacy migrations.

## Non-goals

- Store or transmit credentials.
- Implement Inworld model discovery or inference requests.
- Add Inworld voice-provider metadata.

## Proposed behavior

`AuthStyle::Basic` serializes as `"basic"` and reports `"basic"` from
`AuthStyle::as_str()`. The built-in catalog contains:

```text
slug: inworld
label: Inworld
endpoint: https://api.inworld.ai/v1
auth_style: basic
```

Legacy entries with `type: "inworld"` inherit those values when their modern
fields are absent. `api.inworld.ai` is treated as a built-in inference host and
the endpoint remains chat-completions-only.

## Invariants and constraints

- The catalog is the single source for built-in host classification.
- Only OpenAI's first-party preset advertises the Responses API.
- Existing auth-style wire values remain unchanged.
- No credential value is stored in this crate's provider metadata.

## Acceptance criteria

- All auth styles round-trip through JSON with stable lowercase values.
- Catalog and legacy-migration tests produce the documented Inworld preset.
- Host classification recognizes `api.inworld.ai` and disables the Responses
API fallback for its endpoint.
- The workspace's four contract commands pass.

## Open questions

None.