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
+17 -1
View File
@@ -70,6 +70,9 @@ fn deserialize_response_event(data: &str) -> Result<rs::ResponseStreamEvent> {
Err(first_err) => {
// Try sanitizing: parse as Value, strip unknown tools, retry.
if let Ok(mut value) = serde_json::from_str::<serde_json::Value>(data) {
// A `max` reasoning-effort echo is unrepresentable in the
// typed enum; drop it so the event parses.
kigi_sampling_types::normalize_effort_echo(&mut value);
// Strip tools that async_openai's rs::Tool can't deserialize
// (e.g., xAI-specific "x_search"). Instead of maintaining a
// hardcoded allowlist, try deserializing each tool entry —
@@ -1028,6 +1031,7 @@ impl SamplingClient {
// it in post-serialize. This is the last surviving piece of the
// old raw_output machinery.
kigi_sampling_types::patch_reasoning_text_types(&mut request_body);
kigi_sampling_types::patch_reasoning_effort(&mut request_body, request.reasoning_effort);
let http_request = self.post(self.endpoint("responses")).json(&request_body);
let response = http_request.send().await.map_err(|e| {
@@ -1074,7 +1078,16 @@ impl SamplingClient {
});
}
let response_obj = serde_json::from_slice::<rs::Response>(&bytes).map_err(|e| {
let mut response_value =
serde_json::from_slice::<serde_json::Value>(&bytes).map_err(|e| {
let raw_body = String::from_utf8_lossy(&bytes);
tracing::error!(error = %e, raw_body = %raw_body, "Response body is not JSON");
SamplingError::Serialization(e)
})?;
// A `max` effort echo is unrepresentable in the typed enum — drop it
// rather than failing the whole response.
kigi_sampling_types::normalize_effort_echo(&mut response_value);
let response_obj = serde_json::from_value::<rs::Response>(response_value).map_err(|e| {
let raw_body = String::from_utf8_lossy(&bytes);
tracing::error!(
error = %e,
@@ -1156,6 +1169,7 @@ impl SamplingClient {
}
}
kigi_sampling_types::patch_reasoning_text_types(&mut request_body);
kigi_sampling_types::patch_reasoning_effort(&mut request_body, request.reasoning_effort);
// Fresh per attempt so signals never leak across retries; `None`
// (check disabled) sends no header and does no peek work per event.
let doom_loop = self
@@ -1687,6 +1701,7 @@ impl SamplingClient {
let responses_request: rs::CreateResponse = (&request).into();
let mut wrapper = CreateResponseWrapper::new(responses_request);
wrapper.reasoning_effort = request.reasoning_effort;
wrapper.x_kigi_conv_id = x_kigi_conv_id;
wrapper.x_kigi_req_id = x_kigi_req_id;
wrapper.x_kigi_session_id = x_kigi_session_id;
@@ -1720,6 +1735,7 @@ impl SamplingClient {
let responses_request: rs::CreateResponse = (&request).into();
let mut wrapper = CreateResponseWrapper::new(responses_request);
wrapper.reasoning_effort = request.reasoning_effort;
wrapper.x_kigi_conv_id = x_kigi_conv_id;
wrapper.x_kigi_req_id = x_kigi_req_id;
wrapper.x_kigi_session_id = x_kigi_session_id;