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 .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,11 @@ jobs:
targets: aarch64-linux-android,wasm32-wasip2

- name: Install Android SDK + NDK
uses: android-actions/setup-android@v3
uses: android-actions/setup-android@v4
with:
# The legacy `tools` SDK package was removed from Google's repository.
# This job only needs sdkmanager (installed by the action) plus the NDK.
packages: ''

- name: Install Android NDK
shell: bash
Expand Down
6 changes: 5 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,11 @@ jobs:
targets: aarch64-linux-android,wasm32-wasip2

- name: Install Android SDK + NDK
uses: android-actions/setup-android@v3
uses: android-actions/setup-android@v4
with:
# The legacy `tools` SDK package was removed from Google's repository.
# This job only needs sdkmanager (installed by the action) plus the NDK.
packages: ''

- name: Install Android NDK
shell: bash
Expand Down
23 changes: 16 additions & 7 deletions crates/anvil-client/src/bedrock_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ use crate::llm_client::{
LlmResponse, ModelMetadata, ModelsResponse, OpenAiClient, OutputBudgetExhaustedError,
ReasoningLevelPreset, StreamChatRequest, TokenSink, TokenUsage, ToolCall, ToolDefinition,
};
use crate::responses_api::{build_responses_request, drive_responses_sse_stream};
use crate::responses_api::{
ResponsesRequestOptions, build_responses_request, drive_responses_sse_stream,
};
use crate::responses_chain::{
RESPONSES_CHAIN_CACHE_CAP, ResponsesChainCache, find_responses_continuation,
hash_responses_context, looks_like_expired_previous_response_id,
Expand Down Expand Up @@ -1023,8 +1025,10 @@ impl BedrockClient {
tools.as_deref(),
reasoning_effort.as_deref(),
structured_output.as_ref(),
true,
None,
ResponsesRequestOptions {
store: true,
..Default::default()
},
)
};

Expand All @@ -1037,8 +1041,11 @@ impl BedrockClient {
tools.as_deref(),
reasoning_effort.as_deref(),
structured_output.as_ref(),
true,
Some(previous_response_id.as_str()),
ResponsesRequestOptions {
store: true,
previous_response_id: Some(previous_response_id.as_str()),
..Default::default()
},
);
(body, Some(hash_responses_context(&messages[..*boundary])))
}
Expand Down Expand Up @@ -2983,8 +2990,10 @@ mod tests {
None,
Some("medium"),
None,
true,
None,
ResponsesRequestOptions {
store: true,
..Default::default()
},
)
};
let req_with = serde_json::to_value(build(&messages_with)).unwrap();
Expand Down
7 changes: 4 additions & 3 deletions crates/anvil-client/src/deepseek_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ use anyhow::{Context, Result, bail};
use futures::{StreamExt, future::BoxFuture};

use crate::llm_client::{LlmBackend, LlmResponse, StreamChatRequest};
use crate::responses_api::{build_responses_request, drive_responses_sse_stream};
use crate::responses_api::{
ResponsesRequestOptions, build_responses_request, drive_responses_sse_stream,
};

pub struct DeepSeekClient {
http: reqwest::Client,
Expand Down Expand Up @@ -79,8 +81,7 @@ impl DeepSeekClient {
tools.as_deref(),
effort,
structured_output.as_ref(),
false,
None,
ResponsesRequestOptions::default(),
);
let response = crate::http_retry::send_with_retries(
"posting DeepSeek Responses request",
Expand Down
13 changes: 10 additions & 3 deletions crates/anvil-client/src/discovery.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
//! (`http://localhost:11434/v1/models`), a local ds4-server
//! (antirez/ds4, an OpenAI-compatible DeepSeek V4 inference engine), and
//! Kimi Code, Grok Build OAuth, generic OpenAI-compatible profiles from `providers.json`,
//! and OpenRouter (`https://openrouter.ai/api/v1/models`, gated on the
//! Xiaomi MiMo Token Plan and pay-as-you-go APIs, and OpenRouter
//! (`https://openrouter.ai/api/v1/models`, gated on the
//! `OPENROUTER_API_KEY` env var), and hosted DeepSeek
//! (`https://api.deepseek.com/v1/models`, gated on `DEEPSEEK_API_KEY`).
//!
Expand All @@ -16,6 +17,9 @@
//! the official Grok Build OAuth credential file. Generic
//! OpenAI-compatible profiles are enabled only when `providers.json`
//! configures them.
//! Xiaomi MiMo's plans are enabled by dedicated environment variables (or
//! prefix-disambiguated `MIMO_API_KEY`) and deliberately target separate
//! subscription and pay-as-you-go endpoints.
//!
//! ds4 is the one source whose port is *not* fixed: `ds4-server` has no
//! standard port, so instead of probing a constant we detect a running
Expand All @@ -29,8 +33,9 @@
//! (`MultiBackend`) can pick the right HTTP client at request time. The
//! catalog is presented to ACP clients as `<source>::<id>` wire ids, e.g.
//! `codex::gpt-5-codex`, `ollama::llama3:latest`, `deepseek::deepseek-v4-pro`,
//! `kimi::k3`, `openai::deca/model-id`, and
//! `openrouter::anthropic/claude-3.5-sonnet`. The double-colon separator
//! `kimi::k3`, `openai::deca/model-id`, `mimo::mimo-v2.6-pro`,
//! `mimo-payg::mimo-v2.6-pro`, and `openrouter::anthropic/claude-3.5-sonnet`.
//! The double-colon separator
//! avoids collision with Ollama tags (`model:tag`) and with OpenRouter
//! ids (`vendor/model`).
//!
Expand Down Expand Up @@ -60,6 +65,8 @@ impl ModelSource {
pub const DS4: &'static str = "ds4";
pub const GROK: &'static str = "grok";
pub const KIMI: &'static str = "kimi";
pub const MIMO: &'static str = "mimo";
pub const MIMO_PAYG: &'static str = "mimo-payg";
pub const OLLAMA: &'static str = "ollama";
pub const OPENAI: &'static str = "openai";
pub const OPENROUTER: &'static str = "openrouter";
Expand Down
7 changes: 4 additions & 3 deletions crates/anvil-client/src/grok_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ use crate::llm_client::{
LlmBackend, LlmResponse, ModelMetadata, ReasoningLevelPreset, ResolvedModelInfo,
StreamChatRequest,
};
use crate::responses_api::{ReasoningConfig, build_responses_request, drive_responses_sse_stream};
use crate::responses_api::{
ReasoningConfig, ResponsesRequestOptions, build_responses_request, drive_responses_sse_stream,
};

const GROK_API_BASE_URL: &str = "https://cli-chat-proxy.grok.com/v1";

Expand Down Expand Up @@ -183,8 +185,7 @@ impl GrokClient {
tools.as_deref(),
reasoning_effort.as_deref(),
structured_output.as_ref(),
false,
None,
ResponsesRequestOptions::default(),
);
match body.reasoning.as_mut() {
Some(reasoning) => reasoning.summary = Some("concise".to_string()),
Expand Down
29 changes: 28 additions & 1 deletion crates/anvil-client/src/hosted.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
//! Shared hosted-provider construction for Anvil and language bindings.

use crate::llm_client::LlmBackend;
use crate::{deepseek_auth, discovery, grok_client, kimi_auth, llm_client};
use crate::{deepseek_auth, discovery, grok_client, kimi_auth, llm_client, mimo_client};
use std::sync::Arc;

/// Build a hosted DeepSeek chat backend from a raw API key. DeepSeek's API
Expand Down Expand Up @@ -103,3 +103,30 @@ pub fn build_grok_backend() -> Option<Arc<dyn LlmBackend>> {
}
}
}

/// Build the Xiaomi MiMo Token Plan backend. The generic `MIMO_API_KEY` is
/// accepted when its `tp-` (individual) or `ttp-` (team) prefix identifies
/// a Token Plan credential.
pub fn build_mimo_token_plan_backend() -> Option<Arc<dyn LlmBackend>> {
match mimo_client::MimoClient::load(mimo_client::MimoPlan::TokenPlan) {
Ok(backend) => backend,
Err(error) => {
tracing::warn!("failed to configure Xiaomi MiMo Token Plan authentication: {error:#}");
None
}
}
}

/// Build Xiaomi MiMo's billed pay-as-you-go backend. The generic
/// `MIMO_API_KEY` is accepted only when its `sk-` prefix identifies that plan.
pub fn build_mimo_pay_as_you_go_backend() -> Option<Arc<dyn LlmBackend>> {
match mimo_client::MimoClient::load(mimo_client::MimoPlan::PayAsYouGo) {
Ok(backend) => backend,
Err(error) => {
tracing::warn!(
"failed to configure Xiaomi MiMo pay-as-you-go authentication: {error:#}"
);
None
}
}
}
11 changes: 6 additions & 5 deletions crates/anvil-client/src/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -183,13 +183,15 @@ impl HostedClient {
.map_err(|e| InferError::new(InferErrorKind::Authentication, e))?,
"deepseek" => crate::deepseek_client::DeepSeekClient::load()
.map_err(|e| InferError::new(InferErrorKind::Authentication, e))?,
"mimo" => crate::hosted::build_mimo_token_plan_backend(),
"mimo-payg" => crate::hosted::build_mimo_pay_as_you_go_backend(),
"kimi" => crate::hosted::build_kimi_backend(),
"grok" => crate::hosted::build_grok_backend(),
_ => {
return Err(InferError::new(
InferErrorKind::InvalidRequest,
anyhow!(
"unsupported inference provider {source:?}; expected codex, meta, kimi, grok, or deepseek"
"unsupported inference provider {source:?}; expected codex, meta, kimi, grok, mimo, mimo-payg, or deepseek"
),
));
}
Expand Down Expand Up @@ -278,10 +280,9 @@ pub async fn infer_structured(
// JSON-object fallbacks need an in-band schema, but dynamic schemas must
// follow the caller's stable prompt/article prefix rather than displace it.
if !backend.supports_native_structured_output() {
messages.push(ChatMessage::user(format!(
"Return only JSON matching this JSON Schema: {}",
structured_output.schema,
)));
messages.push(ChatMessage::user(
crate::structured_output::json_schema_instruction(&structured_output),
));
}
let mut total_usage = TokenUsage::default();
let mut validation_attempt = 0;
Expand Down
4 changes: 3 additions & 1 deletion crates/anvil-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
//! - [`infer`]: one-shot, tool-free, schema-constrained inference for callers
//! that do not need an ACP session or Anvil's agent loop.
//! - Provider backends: [`bedrock_client`], [`codex_client`], [`grok_client`],
//! and user-configured OpenAI-compatible endpoints via [`openai_providers`].
//! Xiaomi MiMo Token Plan and pay-as-you-go APIs via [`mimo_client`], and
//! user-configured OpenAI-compatible endpoints via [`openai_providers`].
//! - Auth and credential storage: [`secrets`], [`codex_auth`], [`bedrock_auth`],
//! [`grok_auth`], [`kimi_auth`], [`openrouter_auth`], [`deepseek_auth`].
//! - Credit/balance probes: [`bedrock_credits`], [`codex_credits`],
Expand Down Expand Up @@ -48,6 +49,7 @@ pub mod infer;
pub mod kimi_auth;
pub mod llm_client;
pub mod meta_client;
pub mod mimo_client;
pub mod multi_backend;
pub mod openai_providers;
pub mod openrouter_auth;
Expand Down
7 changes: 4 additions & 3 deletions crates/anvil-client/src/meta_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ use tokio_util::sync::CancellationToken;
use crate::llm_client::{
LlmBackend, LlmResponse, ModelMetadata, ResolvedModelInfo, StreamChatRequest,
};
use crate::responses_api::{build_responses_request, drive_responses_sse_stream};
use crate::responses_api::{
ResponsesRequestOptions, build_responses_request, drive_responses_sse_stream,
};

const META_API_BASE_URL: &str = "https://api.meta.ai/v1";
const META_MINT_BASE_URL: &str = "https://api.meta.ai";
Expand Down Expand Up @@ -298,8 +300,7 @@ impl MetaClient {
tools.as_deref(),
reasoning_effort.as_deref(),
structured_output.as_ref(),
false,
None,
ResponsesRequestOptions::default(),
);
let api_key = self.api_key_for(&cancel).await?;
let response = self
Expand Down
Loading
Loading