Add OpenAI platform: live model fetching with enrichment (provider 1)

The 4th registry row: id "openai", OPENAI_API_KEY env > auth.json
"openai" scope (login picker/paste/validation all registry-generic —
zero TUI changes needed, pinned by the picker test), base
https://api.openai.com/v1 with KIGI_OPENAI_BASE_URL override, Responses
dialect via the new PlatformWireApi spec field, enrichment-backed
metadata (wire_serves_metadata=false).

OpenAI's GET /v1/models returns bare ids and is polluted with
tts/whisper/embeddings entries: the listing is restricted to
enrichment-known TOOL-CALLING models (review caught that membership
alone admitted models.dev-known embeddings models, which would 400 on
every agentic request; dropped ids are debug-logged for launch-day
diagnosability). Context windows, effort menus, display names, and
thinking capability come from the enrichment pipeline — wiremock e2e
pins the full contract: polluted live listing + models.dev →
one Responses-backed chat model with a 400k documented context window.

Responses max-effort wiring (closes the P0c-1 debt): canonical effort
rides a CreateResponseWrapper sidecar and patch_reasoning_effort writes
it onto the serialized body at both send sites (all seven levels pinned,
xhigh/max distinct, summary preserved); normalize_effort_echo drops
echoes async-openai's typed enum cannot represent at both the non-stream
and SSE parse seams; the dead typed to_responses_api converter is
deleted. Kimi/moonshot stay byte-identical (ChatCompletions untouched,
wire_api maps to the same default; kimi wire tests green).

kimi-import now recognizes ANY registry platform host as built-in
(was hardcoded moonshot), covering openai and future rows.
This commit is contained in:
2026-07-21 05:04:55 -04:00
parent fdf9b956f5
commit 23e94939c0
10 changed files with 436 additions and 58 deletions
@@ -2158,7 +2158,10 @@ impl From<&ConversationRequest> for rs::CreateResponse {
prompt_cache_key: None,
prompt_cache_retention: None,
reasoning: Some(rs::Reasoning {
effort: req.reasoning_effort.map(|e| e.to_responses_api()),
// Effort is written onto the serialized body by
// `patch_reasoning_effort` (the typed enum cannot spell
// `max`); the wrapper carries the canonical value.
effort: None,
summary: Some(rs::ReasoningSummary::Concise),
}),
safety_identifier: None,
@@ -5191,6 +5194,10 @@ mod tests {
#[test]
fn test_responses_request_carries_reasoning_effort_nested() {
// Wire contract since the Max split: the typed conversion leaves
// reasoning.effort UNSET (async-openai's enum cannot spell `max`);
// `patch_reasoning_effort` writes the canonical token onto the
// serialized body — every level, `xhigh` and `max` distinct.
for (variant, expected) in [
(crate::ReasoningEffort::None, "none"),
(crate::ReasoningEffort::Minimal, "minimal"),
@@ -5198,6 +5205,7 @@ mod tests {
(crate::ReasoningEffort::Medium, "medium"),
(crate::ReasoningEffort::High, "high"),
(crate::ReasoningEffort::Xhigh, "xhigh"),
(crate::ReasoningEffort::Max, "max"),
] {
let req = ConversationRequest {
reasoning_effort: Some(variant),
@@ -5205,15 +5213,56 @@ mod tests {
.with_model("test")
};
let resp: crate::rs::CreateResponse = (&req).into();
let json = serde_json::to_value(&resp).unwrap();
let mut json = serde_json::to_value(&resp).unwrap();
assert_eq!(
json.pointer("/reasoning/effort"),
None,
"typed conversion must leave effort unset ({variant:?})"
);
crate::patch_reasoning_effort(&mut json, req.reasoning_effort);
assert_eq!(
json.pointer("/reasoning/effort").and_then(|v| v.as_str()),
Some(expected),
"{variant:?} should serialize as reasoning.effort={expected:?}; got: {json:#}",
"{variant:?} should be patched as reasoning.effort={expected:?}; got: {json:#}",
);
assert_eq!(
json.pointer("/reasoning/summary").and_then(|v| v.as_str()),
Some("concise"),
"the patch must not clobber reasoning.summary"
);
}
}
#[test]
fn normalize_effort_echo_drops_only_unrepresentable_tokens() {
// `max` echo (bare response shape) is dropped so typed parsing
// succeeds; known tokens pass through; stream-event envelopes are
// handled via /response/reasoning.
let mut bare = serde_json::json!({ "reasoning": { "effort": "max", "summary": "c" } });
crate::normalize_effort_echo(&mut bare);
assert_eq!(bare.pointer("/reasoning/effort"), None);
assert_eq!(
bare.pointer("/reasoning/summary").and_then(|v| v.as_str()),
Some("c")
);
let mut known = serde_json::json!({ "reasoning": { "effort": "high" } });
crate::normalize_effort_echo(&mut known);
assert_eq!(
known.pointer("/reasoning/effort").and_then(|v| v.as_str()),
Some("high"),
"representable echoes must pass through"
);
let mut event = serde_json::json!({ "response": { "reasoning": { "effort": "max" } } });
crate::normalize_effort_echo(&mut event);
assert_eq!(event.pointer("/response/reasoning/effort"), None);
let mut absent = serde_json::json!({ "output": [] });
crate::normalize_effort_echo(&mut absent);
assert_eq!(absent, serde_json::json!({ "output": [] }));
}
#[test]
fn test_responses_request_omits_effort_when_unset() {
let req =
+58 -28
View File
@@ -794,34 +794,11 @@ pub enum ReasoningEffort {
}
impl ReasoningEffort {
pub fn to_responses_api(self) -> crate::rs::ReasoningEffort {
match self {
Self::None => crate::rs::ReasoningEffort::None,
Self::Minimal => crate::rs::ReasoningEffort::Minimal,
Self::Low => crate::rs::ReasoningEffort::Low,
Self::Medium => crate::rs::ReasoningEffort::Medium,
Self::High => crate::rs::ReasoningEffort::High,
Self::Xhigh => crate::rs::ReasoningEffort::Xhigh,
// INTERIM: async-openai (0.33 pinned; upstream through 0.41
// still lacks it) has no `Max` variant. The Responses backend
// gains true `max` via a post-serialize body patch in the OpenAI
// provider cycle; until then Max downgrades to xhigh — reachable
// via `[model.*] reasoning_effort = "max"` on a responses-backend
// model, hence the loud warn.
Self::Max => {
tracing::warn!(
"reasoning effort `max` downgraded to `xhigh` on the \
Responses backend (typed wire enum has no max yet)"
);
crate::rs::ReasoningEffort::Xhigh
}
}
}
/// Inverse of [`to_responses_api`](Self::to_responses_api): the effort the
/// Responses API echoes back on `response.reasoning.effort`. (`max`
/// echoes cannot reach here — async-openai's enum has no such variant;
/// the OpenAI provider cycle handles them before typed parsing.)
/// The canonical effort behind a typed Responses-API echo
/// (`response.reasoning.effort`). `max` echoes never reach the typed
/// enum — [`normalize_effort_echo`] drops them pre-parse (async-openai
/// has no such variant); the request direction writes the wire string
/// via [`patch_reasoning_effort`].
pub fn from_responses_api(effort: crate::rs::ReasoningEffort) -> Self {
match effort {
crate::rs::ReasoningEffort::None => Self::None,
@@ -911,6 +888,52 @@ where
}))
}
/// Write the canonical effort onto a serialized Responses request body
/// (`body.reasoning.effort`). This is the ONLY place effort reaches the
/// Responses wire: the typed `rs::ReasoningEffort` tops out at `xhigh`, so
/// `max` must be written post-serialize. A `None` effort leaves the body
/// untouched (the provider default applies).
pub fn patch_reasoning_effort(body: &mut Value, effort: Option<ReasoningEffort>) {
let Some(effort) = effort else { return };
let Some(obj) = body.as_object_mut() else {
return;
};
let reasoning = obj
.entry("reasoning")
.or_insert_with(|| Value::Object(serde_json::Map::new()));
if let Some(reasoning) = reasoning.as_object_mut() {
reasoning.insert(
"effort".to_string(),
Value::String(effort.as_str().to_string()),
);
}
}
/// Neutralize a `reasoning.effort` echo the typed `rs` enum cannot parse
/// (`max`): remove it so response deserialization succeeds. The turn's
/// canonical effort lives in the session sampling config regardless; only
/// the per-item echo metadata is dropped, recorded as not-echoed.
/// (Ceiling: async-openai lacks a Max variant; delete this when it grows
/// one.) Handles both bare response bodies (`/reasoning/effort`) and
/// stream-event envelopes (`/response/reasoning/effort`).
pub fn normalize_effort_echo(value: &mut Value) {
for path in ["/reasoning", "/response/reasoning"] {
if let Some(reasoning) = value.pointer_mut(path).and_then(|v| v.as_object_mut())
&& let Some(effort) = reasoning.get("effort").and_then(|v| v.as_str())
&& crate::rs::ReasoningEffort::deserialize(serde_json::Value::String(
effort.to_string(),
))
.is_err()
{
tracing::debug!(
effort,
"reasoning.effort echo unrepresentable in the typed enum; dropping"
);
reasoning.remove("effort");
}
}
}
pub const REASONING_EFFORT_META_KEY: &str = "reasoningEffort";
pub const SUPPORTS_REASONING_EFFORT_META_KEY: &str = "supportsReasoningEffort";
@@ -1126,6 +1149,12 @@ pub struct CreateResponseWrapper {
/// The inner Responses API request.
pub inner: crate::rs::CreateResponse,
/// Canonical reasoning effort for this request. The typed
/// `inner.reasoning.effort` stays `None` (async-openai's enum cannot
/// spell `max`); [`patch_reasoning_effort`] writes this value onto the
/// serialized body just before send.
pub reasoning_effort: Option<ReasoningEffort>,
/// Custom header: conversation ID for tracking.
pub x_kigi_conv_id: Option<String>,
@@ -1152,6 +1181,7 @@ impl CreateResponseWrapper {
pub fn new(inner: crate::rs::CreateResponse) -> Self {
Self {
inner,
reasoning_effort: None,
x_kigi_conv_id: None,
x_kigi_req_id: None,
x_kigi_session_id: None,