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).
|
/// (OpenAI-compatible, Z.ai, OpenCode).
|
||||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
pub url: Option<String>,
|
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
|
/// 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()
|
.clone()
|
||||||
.unwrap_or_else(|| rhema_config::Profile::default().llama_cpp_url());
|
.unwrap_or_else(|| rhema_config::Profile::default().llama_cpp_url());
|
||||||
let built = match kind {
|
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
|
// 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(
|
"openai" | "zai" | "opencode" => build(
|
||||||
kind,
|
kind,
|
||||||
Some(require_key(kind, params)?.as_str()),
|
Some(require_key(kind, params)?.as_str()),
|
||||||
None,
|
None,
|
||||||
params.url.as_deref(),
|
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 {
|
.map_err(|err| match err {
|
||||||
rhema_provider_registry::BuildError::Unknown(kind) => BackendError::UnknownKind(kind),
|
rhema_provider_registry::BuildError::Unknown(kind) => BackendError::UnknownKind(kind),
|
||||||
@@ -559,12 +580,14 @@ pub fn discover(
|
|||||||
kind: kind.to_owned(),
|
kind: kind.to_owned(),
|
||||||
api_key: None,
|
api_key: None,
|
||||||
url: Some(stored.llama_cpp_url()),
|
url: Some(stored.llama_cpp_url()),
|
||||||
|
api: None,
|
||||||
}
|
}
|
||||||
} else if let Some(api_key) = env_key(kind) {
|
} else if let Some(api_key) = env_key(kind) {
|
||||||
ProviderParams {
|
ProviderParams {
|
||||||
kind: kind.to_owned(),
|
kind: kind.to_owned(),
|
||||||
api_key: Some(api_key),
|
api_key: Some(api_key),
|
||||||
url: stored_route(kind),
|
url: stored_route(kind),
|
||||||
|
api: None,
|
||||||
}
|
}
|
||||||
} else if stored_of_kind {
|
} else if stored_of_kind {
|
||||||
// The stored instance of the kind wins over the profile
|
// The stored instance of the kind wins over the profile
|
||||||
@@ -582,6 +605,7 @@ pub fn discover(
|
|||||||
kind: kind.to_owned(),
|
kind: kind.to_owned(),
|
||||||
api_key,
|
api_key,
|
||||||
url: stored_route(kind),
|
url: stored_route(kind),
|
||||||
|
api: None,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let has_creds = match kind {
|
let has_creds = match kind {
|
||||||
@@ -733,6 +757,7 @@ mod tests {
|
|||||||
kind: kind.to_owned(),
|
kind: kind.to_owned(),
|
||||||
api_key: api_key.map(str::to_owned),
|
api_key: api_key.map(str::to_owned),
|
||||||
url: url.map(str::to_owned),
|
url: url.map(str::to_owned),
|
||||||
|
api: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -853,6 +878,7 @@ mod tests {
|
|||||||
api_key: None,
|
api_key: None,
|
||||||
secret_ref: Some("ref-1".into()),
|
secret_ref: Some("ref-1".into()),
|
||||||
base_url: None,
|
base_url: None,
|
||||||
|
api: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
let store = rhema_config::secrets::VolatileSecretStore::new();
|
let store = rhema_config::secrets::VolatileSecretStore::new();
|
||||||
@@ -961,6 +987,7 @@ mod tests {
|
|||||||
api_key: Some("pg".into()),
|
api_key: Some("pg".into()),
|
||||||
secret_ref: None,
|
secret_ref: None,
|
||||||
base_url: None,
|
base_url: None,
|
||||||
|
api: None,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
profile
|
profile
|
||||||
|
|||||||
@@ -2094,6 +2094,7 @@ async fn providers_set_credentials(
|
|||||||
kind: kind.clone(),
|
kind: kind.clone(),
|
||||||
api_key: blank_is_absent(body["api_key"].as_str()),
|
api_key: blank_is_absent(body["api_key"].as_str()),
|
||||||
url: blank_is_absent(body["url"].as_str()),
|
url: blank_is_absent(body["url"].as_str()),
|
||||||
|
api: None,
|
||||||
};
|
};
|
||||||
let mut backend = crate::backends::construct(&kind, ¶ms).map_err(bad_request)?;
|
let mut backend = crate::backends::construct(&kind, ¶ms).map_err(bad_request)?;
|
||||||
backend.id = id.to_owned();
|
backend.id = id.to_owned();
|
||||||
|
|||||||
Reference in new issue
Block a user