feat(inference): flip openai/codex + custom cloud slugs to crate-native OpenAiModel (#4782)

This commit is contained in:
Steven Enamakel
2026-07-10 21:04:01 -07:00
committed by GitHub
parent 9a7d91f833
commit e7a6b616b3
5 changed files with 124 additions and 40 deletions
+1 -1
View File
@@ -79,7 +79,7 @@ site is crate-backed (the migration's scaffold → flip → delete pattern).
| --- | --- | --- |
| Managed OpenHuman backend (common path: chat turns, memory/learning/meeting summaries) | `factory::make_openhuman_backend_model``OpenHumanBackendModel` (dynamic JWT + `thread_id` + billing envelope bridged onto crate `OpenAiModel`) | **CUT OVER**`create_chat_model_with_model_id` routes it (commit `7e98c1b39`); test-provider override still wins. |
| Wire-equivalent BYOK cloud slug (Anthropic / None / plain-Bearer, no codex-oauth, no `/v1/responses`) | `factory::try_create_cloud_slug_chat_model``crate_openai::make_crate_openai_chat_model` | **CUT OVER (conservative subset)**`create_chat_model` routes these crate-native after the managed + local short-circuits. Resolution is shared via `resolve_cloud_slug` (the legacy `make_cloud_provider_by_slug` was refactored onto it, so eligible slugs resolve **identically**; only the wire client differs). The same `enforce_local_only_inference` + `verify_session_active` gate runs first. Covers the common non-OpenAI BYOK providers (DeepSeek, Groq, Mistral, xAI, …) via the crate wire the managed backend already proves. |
| `openai` / codex + custom-proxy cloud slugs | stay `Provider` path | **PHASE 3**`openai` keeps the `/v1/responses` fallback + codex-oauth (query-param auth, account/originator headers, user-agent, responses-API-primary); custom slugs may proxy `/v1/responses`. `try_create_cloud_slug_chat_model` returns `None` for these. Land with the crate `/responses` port. |
| `openai` / codex + custom-proxy cloud slugs | crate-native via `try_create_cloud_slug_chat_model` | **CUT OVER** — the flip now covers **every** configured cloud slug except the managed `OpenhumanJwt` entry. Codex OAuth → crate `OpenAiModel` on the Responses API (`with_responses_api_primary` + account/originator headers + user-agent + `client_version` query + `max_output_tokens` omitted), enabled by the crate `/v1/responses` port ([tinyagents#51](https://github.com/tinyhumansai/tinyagents/pull/51)). Non-codex `openai` + custom slugs → crate Chat Completions (the legacy 404 → `/v1/responses` **fallback** is not replicated — chat completions is their primary path). Host pin `8e57665` = #49 toggles + #51 Responses, with #50 reverted (#52) so loopback stays retryable. **Live per-provider validation deferred to a dedicated tinyagents run.** |
| Local runtimes (Ollama/LM Studio/MLX/OMLX/local-openai) | `factory::try_create_local_runtime_chat_model``crate_openai::make_crate_local_runtime_chat_model` (native tools + vision forced off; `num_ctx` baked as `{"options":{"num_ctx":N}}`) | **CUT OVER**`create_chat_model_with_model_id` routes local runtimes crate-native (after the managed short-circuit). The flip **re-runs the same gate** the `Provider` path applies (`enforce_local_only_inference` + `verify_session_active`), so it cannot bypass privacy mode or the session requirement. Temperature rides the per-call `ModelRequest` (parity with managed). **Loopback error handling defers to upstream:** an upstream merge (`b709a993…`/`04ffc029…`) replaced the earlier `..._offline_trips_halt_guard` test with `cron_agent_job_short_loopback_send_error_stays_retryable` — i.e. an offline local provider now **stays retryable** (it may be transiently starting up). So the transient cron `{e:#}` cause-chain surfacing + the `is_non_retryable` loopback fast-fail were reverted, and the host stays pinned at `7c6e81a` (before [tinyagents#50](https://github.com/tinyhumansai/tinyagents/pull/50) `error_source_chain`) so the crate-native local error does not surface the `connection refused` errno the classifier would trip on. #50 remains a good crate improvement but is deliberately **not consumed** here to keep loopback retryable. |
| Bespoke (managed backend, `claude_code`, `claude_agent_sdk`, `openai_codex`) | stay host `ChatModel` impls | **HOST-OWNED** — subprocess / `/v1/responses` / query-param auth have no crate equivalent; never route through `crate_openai`. |
@@ -79,6 +79,19 @@ pub(crate) struct CrateOpenAiConfig<'a> {
/// `provider_options`. `None` bakes nothing. Maps to
/// `OpenAiModel::with_default_provider_options`.
pub default_provider_options: Option<serde_json::Value>,
/// Route calls to the OpenAI **Responses API** (`/v1/responses`) instead of
/// Chat Completions — the OpenAI Codex OAuth backend. Maps to
/// `OpenAiModel::with_responses_api_primary`.
pub responses_api_primary: bool,
/// (Responses path) omit `max_output_tokens`, which the Codex backend
/// rejects. Maps to `OpenAiModel::with_responses_omit_max_output_tokens`.
pub responses_omit_max_output_tokens: bool,
/// Static query parameters appended to every request URL (e.g. the Codex
/// `client_version`). Maps to `OpenAiModel::with_extra_query_param`.
pub extra_query_params: &'a [(String, String)],
/// `User-Agent` header override (e.g. the Codex CLI UA). Maps to
/// `OpenAiModel::with_user_agent`.
pub user_agent: Option<&'a str>,
}
/// Build a crate-native `OpenAiModel` (`ChatModel`) for the given OpenAI-compatible
@@ -118,6 +131,18 @@ pub(crate) fn build_crate_openai_model(config: CrateOpenAiConfig<'_>) -> Arc<dyn
if let Some(options) = config.default_provider_options {
model = model.with_default_provider_options(options);
}
for (name, value) in config.extra_query_params {
model = model.with_extra_query_param(name.clone(), value.clone());
}
if let Some(user_agent) = config.user_agent {
model = model.with_user_agent(user_agent);
}
if config.responses_api_primary {
model = model.with_responses_api_primary();
}
if config.responses_omit_max_output_tokens {
model = model.with_responses_omit_max_output_tokens();
}
Arc::new(model)
}
@@ -157,6 +182,10 @@ pub(crate) fn make_crate_openai_chat_model(
native_tool_calling: None,
vision: None,
default_provider_options: None,
responses_api_primary: false,
responses_omit_max_output_tokens: false,
extra_query_params: &[],
user_agent: None,
})
}
@@ -197,6 +226,10 @@ pub(crate) fn make_crate_local_runtime_chat_model(
native_tool_calling: Some(false),
vision: Some(false),
default_provider_options,
responses_api_primary: false,
responses_omit_max_output_tokens: false,
extra_query_params: &[],
user_agent: None,
})
}
@@ -240,6 +273,10 @@ mod tests {
native_tool_calling: None,
vision: None,
default_provider_options: None,
responses_api_primary: false,
responses_omit_max_output_tokens: false,
extra_query_params: &[],
user_agent: None,
});
// The built model carries the configured provider + model on its profile.
let profile = model.profile().expect("openai models expose a profile");
@@ -281,6 +318,10 @@ mod tests {
native_tool_calling: Some(false),
vision: Some(false),
default_provider_options: None,
responses_api_primary: false,
responses_omit_max_output_tokens: false,
extra_query_params: &[],
user_agent: None,
});
}
+67 -33
View File
@@ -2236,22 +2236,23 @@ fn make_cloud_provider_by_slug(
}
/// A `<slug>:<model>` BYOK cloud provider as a crate-native [`ChatModel`] — the
/// Motion B cutover of the wire-equivalent [`make_cloud_provider_by_slug`]
/// branches (issue #4727 Phase 3, conservative subset).
/// Motion B cutover of every [`make_cloud_provider_by_slug`] branch except the
/// managed `OpenhumanJwt` entry (issue #4727 Phase 3).
///
/// Returns `None` (fall through to the `Provider` path) unless the role resolves
/// to a **configured** cloud slug whose auth style the crate `OpenAiModel` serves
/// with byte-identical wire semantics:
/// - `Anthropic` / `None` auth → always eligible (their endpoints have no
/// `/v1/responses`, so the host's dormant responses-fallback is behavior-neutral);
/// - `Bearer` → eligible **only** when there is no `/v1/responses` fallback, no
/// `openai-codex` OAuth, and no codex account header — i.e. a plain
/// chat-completions Bearer provider (DeepSeek, Groq, Mistral, xAI, …).
/// to a **configured** cloud slug. When it does:
/// - `Anthropic` / `None` / plain `Bearer` → crate `OpenAiModel` Chat Completions;
/// - `Bearer` with OpenAI **Codex OAuth**crate `OpenAiModel` on the Responses
/// API (`with_responses_api_primary`), with the codex account/originator
/// headers, user-agent, `client_version` query param, and `max_output_tokens`
/// omitted (the crate `/v1/responses` support, tinyagents#51);
/// - `OpenhumanJwt` → `None` (routed to the managed backend elsewhere).
///
/// Everything else — `openai` (codex-oauth + Responses API), custom slugs whose
/// endpoint may proxy `/v1/responses`, and the managed `OpenhumanJwt` entry —
/// stays on the `Provider` path (deferred to the crate `/responses` port). The
/// resolution is shared via [`resolve_cloud_slug`], so eligible slugs resolve
/// The legacy host's rare chat-completions-404 → `/v1/responses` **fallback** for
/// non-codex slugs is not replicated (the crate has responses-*primary*, not
/// fallback); chat completions is the primary path those slugs use in practice.
///
/// The resolution is shared via [`resolve_cloud_slug`], so slugs resolve
/// identically to the legacy path; only the wire client differs. The **same**
/// access gate the `Provider` path applies (`enforce_local_only_inference` +
/// `verify_session_active`) runs before building. Temperature rides the per-call
@@ -2311,36 +2312,69 @@ fn try_create_cloud_slug_chat_model(
Err(e) => return Some(Err(e)),
};
// Only flip the wire-equivalent auth styles; codex-oauth, the `/v1/responses`
// fallback, and the managed `OpenhumanJwt` entry stay on the `Provider` path.
// Every configured cloud slug except the managed `OpenhumanJwt` entry builds
// a crate-native client. Codex OAuth routes to the Responses API with its
// headers / UA / query; every other Bearer/Anthropic/None slug uses Chat
// Completions (its primary path — the legacy host's rare 404 → `/v1/responses`
// fallback for non-codex slugs is not replicated).
let mut endpoint = entry.endpoint.clone();
let mut extra_headers: Vec<(String, String)> = Vec::new();
let mut extra_query_params: Vec<(String, String)> = Vec::new();
let mut user_agent: Option<String> = None;
let mut responses_api_primary = false;
let mut responses_omit_max_output_tokens = false;
let auth = match entry.auth_style {
AuthStyle::Anthropic => CompatAuthStyle::Anthropic,
AuthStyle::None => CompatAuthStyle::None,
AuthStyle::OpenhumanJwt => return None,
AuthStyle::Bearer => {
let responses_fallback = (!is_builtin_cloud_slug(&slug)
|| builtin_cloud_supports_responses_api(&slug))
&& !endpoint_host_is_chat_completions_only(&codex.endpoint);
if responses_fallback || codex.using_oauth || codex.account_id.is_some() {
return None;
// The codex routing may re-target the endpoint (OAuth backend).
endpoint = codex.endpoint.clone();
if let Some(account_id) = codex.account_id.as_deref() {
extra_headers.push((
OPENAI_CODEX_ACCOUNT_HEADER.to_string(),
account_id.to_string(),
));
}
if codex.using_oauth {
// Codex OAuth → Responses API primary + the codex request shape.
extra_headers.push((
OPENAI_CODEX_ORIGINATOR_HEADER.to_string(),
OPENAI_CODEX_ORIGINATOR.to_string(),
));
user_agent = Some(openai_codex_user_agent());
extra_query_params
.push(("client_version".to_string(), openai_codex_client_version()));
responses_api_primary = true;
responses_omit_max_output_tokens = true;
}
CompatAuthStyle::Bearer
}
};
let unsupported = &config.temperature_unsupported_models;
let chat = super::crate_openai::make_crate_openai_chat_model(
&slug,
&entry.endpoint,
&key,
auth,
&effective_model,
unsupported,
temperature_override,
// Cloud OpenAI-compatible providers accept a `system` role — no merge
// (parity with `OpenAiCompatibleProvider::new`).
false,
);
let unsupported = config.temperature_unsupported_models.clone();
let chat =
super::crate_openai::build_crate_openai_model(super::crate_openai::CrateOpenAiConfig {
provider_name: slug.as_str(),
endpoint: endpoint.as_str(),
api_key: key.as_str(),
auth_style: auth,
model: effective_model.as_str(),
temperature_unsupported_models: unsupported.as_slice(),
temperature_override,
// Cloud OpenAI-compatible providers accept a `system` role — no merge
// (parity with `OpenAiCompatibleProvider::new`).
merge_system_into_user: false,
extra_headers: extra_headers.as_slice(),
native_tool_calling: None,
vision: None,
default_provider_options: None,
responses_api_primary,
responses_omit_max_output_tokens,
extra_query_params: extra_query_params.as_slice(),
user_agent: user_agent.as_deref(),
});
Some(Ok((chat, effective_model)))
}
@@ -2956,17 +2956,26 @@ fn create_chat_model_routes_anthropic_auth_cloud_slug_to_crate_native() {
}
#[test]
fn try_create_cloud_slug_declines_openai_and_non_cloud() {
fn try_create_cloud_slug_flips_openai_but_declines_non_cloud() {
use tinyagents::harness::model::ChatModel;
let _guard = crate::openhuman::inference::inference_test_guard();
// `openai` exposes the Responses API, so its Bearer branch keeps the
// `/v1/responses` fallback — it is NOT wire-equivalent and stays host-side.
// `openai` (API-key Bearer, no codex OAuth) now flips crate-native on Chat
// Completions — the legacy `/v1/responses` fallback is not replicated.
let mut openai = Config::default();
openai.cloud_providers.push(openai_entry("p_oai", "openai"));
openai.chat_provider = Some("openai:gpt-4o-mini".to_string());
assert!(try_create_cloud_slug_chat_model("chat", &openai).is_none());
let (model, model_id) = try_create_cloud_slug_chat_model("chat", &openai)
.expect("openai should flip crate-native")
.expect("build");
assert_eq!(model_id, "gpt-4o-mini");
assert_eq!(
model.profile().and_then(|p| p.provider.as_deref()),
Some("openai")
);
// Managed (default), local runtimes, and unconfigured slugs are not cloud
// slugs — they fall through to their own paths.
// slugs — they decline and fall through to their own paths.
assert!(try_create_cloud_slug_chat_model("chat", &Config::default()).is_none());
let mut local = Config::default();
local.chat_provider = Some("ollama:qwen2.5".to_string());