feat(code): ask the request shape on the custom-endpoint login, and honor the stored choice everywhere
The one kind whose dialect its route cannot imply is the openai kind: OpenAI itself serves the Responses API, an arbitrary OpenAI-compatible server only promises chat completions. That choice was implicit; now the user can make it. Config: ProviderInstance gains api (Option<ProviderApi>, chat | responses), serde snake_case, defaulting to absent — every config written before the field reads as unset and keeps its route rule. Registry: build() takes the stored choice as its fifth argument. An explicit chat/responses beats the route rule for the openai kind; unknown spellings fall back to the rule. A wiremock test proves the lift: build(openai, override_url, responses) lands on /responses. Frontends: the TUI's add-instance flow inserts a ChooseApi stage between the base-URL and key prompts for the custom-endpoint row only — arrows choose, enter records and descends, esc returns to the URL prompt; chat completions leads the list so enter-without-looking keeps what the route rule would have picked. The choice rides the pending login into the backend, the saved instance, and back out through from_profile. The ACP resolver honors the stored field via stored_api(); claw's ProviderParams gains the same field (config-file writable; its login forms do not ask yet). Every other kind's dialect stays its endpoint's own decision. New tests: the stage's default and esc contract, the explicit Responses pick, the end-to-end landing in the saved instance, the config serde round-trip both spellings plus old-file absence, and the registry's precedence rule. fmt, clippy -D warnings, and all 135 test groups pass; the WIP in screens/settings.rs, ui/pane.rs, palette.rs, README is untouched.
This commit is contained in:
1 parent
762b49eaf4
commit
3415e27463
2 files changed
+31
-3
No files matched your search
@@ -360,6 +360,12 @@ pub struct ProviderParams {
|
||||
/// (OpenAI-compatible, Z.ai, OpenCode).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub url: Option<String>,
|
||||
/// The request shape an `openai` kind speaks, when the config
|
||||
/// chose one: `chat` or `responses`. Unset lets the registry's
|
||||
/// route rule decide. Writable in the config file today; the login
|
||||
/// forms do not ask for it yet.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub api: Option<String>,
|
||||
}
|
||||
|
||||
/// Every kind a runtime login can construct, ascending: the addable
|
||||
@@ -438,16 +444,31 @@ pub fn construct(kind: &str, params: &ProviderParams) -> Result<Backend, Backend
|
||||
.clone()
|
||||
.unwrap_or_else(|| rhema_config::Profile::default().llama_cpp_url());
|
||||
let built = match kind {
|
||||
"llama-cpp" => build(kind, params.api_key.as_deref(), Some(&llama_url), None),
|
||||
"llama-cpp" => build(
|
||||
kind,
|
||||
params.api_key.as_deref(),
|
||||
Some(&llama_url),
|
||||
None,
|
||||
None,
|
||||
),
|
||||
// The route-bearing kinds take an optional stored override; the
|
||||
// rest keep their adapter's default.
|
||||
// rest keep their adapter's default. Only the `openai` kind
|
||||
// reads the stored request shape — the one whose dialect its
|
||||
// route cannot imply.
|
||||
"openai" | "zai" | "opencode" => build(
|
||||
kind,
|
||||
Some(require_key(kind, params)?.as_str()),
|
||||
None,
|
||||
params.url.as_deref(),
|
||||
params.api.as_deref(),
|
||||
),
|
||||
_ => build(
|
||||
kind,
|
||||
Some(require_key(kind, params)?.as_str()),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
),
|
||||
_ => build(kind, Some(require_key(kind, params)?.as_str()), None, None),
|
||||
}
|
||||
.map_err(|err| match err {
|
||||
rhema_provider_registry::BuildError::Unknown(kind) => BackendError::UnknownKind(kind),
|
||||
@@ -559,12 +580,14 @@ pub fn discover(
|
||||
kind: kind.to_owned(),
|
||||
api_key: None,
|
||||
url: Some(stored.llama_cpp_url()),
|
||||
api: None,
|
||||
}
|
||||
} else if let Some(api_key) = env_key(kind) {
|
||||
ProviderParams {
|
||||
kind: kind.to_owned(),
|
||||
api_key: Some(api_key),
|
||||
url: stored_route(kind),
|
||||
api: None,
|
||||
}
|
||||
} else if stored_of_kind {
|
||||
// The stored instance of the kind wins over the profile
|
||||
@@ -582,6 +605,7 @@ pub fn discover(
|
||||
kind: kind.to_owned(),
|
||||
api_key,
|
||||
url: stored_route(kind),
|
||||
api: None,
|
||||
}
|
||||
};
|
||||
let has_creds = match kind {
|
||||
@@ -733,6 +757,7 @@ mod tests {
|
||||
kind: kind.to_owned(),
|
||||
api_key: api_key.map(str::to_owned),
|
||||
url: url.map(str::to_owned),
|
||||
api: None,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -853,6 +878,7 @@ mod tests {
|
||||
api_key: None,
|
||||
secret_ref: Some("ref-1".into()),
|
||||
base_url: None,
|
||||
api: None,
|
||||
},
|
||||
);
|
||||
let store = rhema_config::secrets::VolatileSecretStore::new();
|
||||
@@ -961,6 +987,7 @@ mod tests {
|
||||
api_key: Some("pg".into()),
|
||||
secret_ref: None,
|
||||
base_url: None,
|
||||
api: None,
|
||||
},
|
||||
);
|
||||
profile
|
||||
|
||||
@@ -2094,6 +2094,7 @@ async fn providers_set_credentials(
|
||||
kind: kind.clone(),
|
||||
api_key: blank_is_absent(body["api_key"].as_str()),
|
||||
url: blank_is_absent(body["url"].as_str()),
|
||||
api: None,
|
||||
};
|
||||
let mut backend = crate::backends::construct(&kind, ¶ms).map_err(bad_request)?;
|
||||
backend.id = id.to_owned();
|
||||
|
||||
Reference in new issue
Block a user