fix(opencode): make models.dev providers cache-independent
This commit is contained in:
@ -4487,6 +4487,31 @@ command = "idea-mcp"
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn opencode_provider_config_json_zai_is_cache_independent() {
|
||||||
|
let custom = domain::profile::CustomProviderConfig::new(
|
||||||
|
"@ai-sdk/openai-compatible",
|
||||||
|
"https://api.z.ai/api/paas/v4",
|
||||||
|
Some("GLM-5.1".to_owned()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let config =
|
||||||
|
OpenCodeProviderConfig::new("zai", "glm-5.1", domain::ports::SecretRef::new("ref"))
|
||||||
|
.unwrap()
|
||||||
|
.with_custom(custom);
|
||||||
|
let body = opencode_provider_config_json(&config, "secret", "/project", None, None);
|
||||||
|
assert_eq!(body["model"], "zai/glm-5.1");
|
||||||
|
assert_eq!(body["provider"]["zai"]["npm"], "@ai-sdk/openai-compatible");
|
||||||
|
assert_eq!(
|
||||||
|
body["provider"]["zai"]["options"]["baseURL"],
|
||||||
|
"https://api.z.ai/api/paas/v4"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
body["provider"]["zai"]["models"]["glm-5.1"]["name"],
|
||||||
|
"GLM-5.1"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- permission projection wiring (ticket #94) -----------------------
|
// ---- permission projection wiring (ticket #94) -----------------------
|
||||||
|
|
||||||
/// Builds an [`EffectivePermissions`] with the given fallback posture (only
|
/// Builds an [`EffectivePermissions`] with the given fallback posture (only
|
||||||
@ -4515,8 +4540,7 @@ command = "idea-mcp"
|
|||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
use domain::permission::Posture;
|
use domain::permission::Posture;
|
||||||
let allowed =
|
let allowed = opencode_config_json(&config, "/project", None, Some(&eff(Posture::Allow)));
|
||||||
opencode_config_json(&config, "/project", None, Some(&eff(Posture::Allow)));
|
|
||||||
assert_eq!(allowed["permission"]["bash"], "allow");
|
assert_eq!(allowed["permission"]["bash"], "allow");
|
||||||
assert_eq!(allowed["permission"]["edit"], "allow");
|
assert_eq!(allowed["permission"]["edit"], "allow");
|
||||||
|
|
||||||
|
|||||||
@ -29,19 +29,19 @@ pub use catalogue::{
|
|||||||
reference_profile_id, reference_profiles, selectable_reference_profiles, CODEX_SUBMIT_DELAY_MS,
|
reference_profile_id, reference_profiles, selectable_reference_profiles, CODEX_SUBMIT_DELAY_MS,
|
||||||
};
|
};
|
||||||
pub use inspect::{InspectConversation, InspectConversationInput, InspectConversationOutput};
|
pub use inspect::{InspectConversation, InspectConversationInput, InspectConversationOutput};
|
||||||
pub use provider_catalogue::{
|
|
||||||
opencode_provider_catalogue, ListOpenCodeProviders, ListOpenCodeProvidersOutput,
|
|
||||||
OpenCodeProviderCatalogEntry,
|
|
||||||
};
|
|
||||||
pub use lifecycle::{
|
pub use lifecycle::{
|
||||||
ChangeAgentProfile, ChangeAgentProfileInput, ChangeAgentProfileOutput, CreateAgentFromScratch,
|
resolve_opencode_mcp_timeout_ms, ChangeAgentProfile, ChangeAgentProfileInput,
|
||||||
CreateAgentInput, CreateAgentOutput, DeleteAgent, DeleteAgentInput, HandoffProvider,
|
ChangeAgentProfileOutput, CreateAgentFromScratch, CreateAgentInput, CreateAgentOutput,
|
||||||
InjectedLiveRow, LaunchAgent, LaunchAgentInput, LaunchAgentOutput, ListAgents, ListAgentsInput,
|
DeleteAgent, DeleteAgentInput, HandoffProvider, InjectedLiveRow, LaunchAgent, LaunchAgentInput,
|
||||||
ListAgentsOutput, LiveStateLeanProvider, McpRuntime, PermissionProjectorRegistry,
|
LaunchAgentOutput, ListAgents, ListAgentsInput, ListAgentsOutput, LiveStateLeanProvider,
|
||||||
ProviderSessionProvider, ReadAgentContext, ReadAgentContextInput, ReadAgentContextOutput,
|
McpRuntime, PermissionProjectorRegistry, ProviderSessionProvider, ReadAgentContext,
|
||||||
StructuredRoutingMode, StructuredSessionDescriptor, UpdateAgentContext,
|
ReadAgentContextInput, ReadAgentContextOutput, StructuredRoutingMode,
|
||||||
UpdateAgentContextInput, DEFAULT_OPENCODE_MCP_TIMEOUT_MS, AGENT_MEMORY_RECALL_BUDGET,
|
StructuredSessionDescriptor, UpdateAgentContext, UpdateAgentContextInput,
|
||||||
LIVE_STATE_INJECT_MAX, resolve_opencode_mcp_timeout_ms,
|
AGENT_MEMORY_RECALL_BUDGET, DEFAULT_OPENCODE_MCP_TIMEOUT_MS, LIVE_STATE_INJECT_MAX,
|
||||||
|
};
|
||||||
|
pub use provider_catalogue::{
|
||||||
|
opencode_models_cache_path, opencode_provider_catalogue, ListOpenCodeProviders,
|
||||||
|
ListOpenCodeProvidersOutput, OpenCodeProviderCatalogEntry,
|
||||||
};
|
};
|
||||||
pub use resume::{
|
pub use resume::{
|
||||||
ListResumableAgents, ListResumableAgentsInput, ListResumableAgentsOutput, ResumableAgent,
|
ListResumableAgents, ListResumableAgentsInput, ListResumableAgentsOutput, ResumableAgent,
|
||||||
|
|||||||
@ -21,6 +21,10 @@ use std::path::PathBuf;
|
|||||||
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
use domain::profile::CustomProviderConfig;
|
||||||
|
|
||||||
|
const BUILT_IN_PROVIDERS: [&str; 3] = ["anthropic", "openai", "openrouter"];
|
||||||
|
|
||||||
/// One entry of the OpenCode cloud-provider catalogue.
|
/// One entry of the OpenCode cloud-provider catalogue.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct OpenCodeProviderCatalogEntry {
|
pub struct OpenCodeProviderCatalogEntry {
|
||||||
@ -30,6 +34,11 @@ pub struct OpenCodeProviderCatalogEntry {
|
|||||||
pub display_name: String,
|
pub display_name: String,
|
||||||
/// Model identifiers this provider serves, offered for selection.
|
/// Model identifiers this provider serves, offered for selection.
|
||||||
pub models: Vec<String>,
|
pub models: Vec<String>,
|
||||||
|
/// Runtime metadata kept inside the application layer. It is deliberately
|
||||||
|
/// not exposed by the existing frontend DTO.
|
||||||
|
pub(crate) npm: Option<String>,
|
||||||
|
pub(crate) api: Option<String>,
|
||||||
|
pub(crate) model_labels: BTreeMap<String, String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The original lot-B3 catalogue: three well-known providers, used whenever
|
/// The original lot-B3 catalogue: three well-known providers, used whenever
|
||||||
@ -44,6 +53,9 @@ fn static_fallback_catalogue() -> Vec<OpenCodeProviderCatalogEntry> {
|
|||||||
"claude-opus-4-8".to_owned(),
|
"claude-opus-4-8".to_owned(),
|
||||||
"claude-haiku-4-5-20251001".to_owned(),
|
"claude-haiku-4-5-20251001".to_owned(),
|
||||||
],
|
],
|
||||||
|
npm: None,
|
||||||
|
api: None,
|
||||||
|
model_labels: BTreeMap::new(),
|
||||||
},
|
},
|
||||||
OpenCodeProviderCatalogEntry {
|
OpenCodeProviderCatalogEntry {
|
||||||
provider_id: "openrouter".to_owned(),
|
provider_id: "openrouter".to_owned(),
|
||||||
@ -53,11 +65,17 @@ fn static_fallback_catalogue() -> Vec<OpenCodeProviderCatalogEntry> {
|
|||||||
"openai/gpt-5".to_owned(),
|
"openai/gpt-5".to_owned(),
|
||||||
"google/gemini-3-pro".to_owned(),
|
"google/gemini-3-pro".to_owned(),
|
||||||
],
|
],
|
||||||
|
npm: None,
|
||||||
|
api: None,
|
||||||
|
model_labels: BTreeMap::new(),
|
||||||
},
|
},
|
||||||
OpenCodeProviderCatalogEntry {
|
OpenCodeProviderCatalogEntry {
|
||||||
provider_id: "openai".to_owned(),
|
provider_id: "openai".to_owned(),
|
||||||
display_name: "OpenAI".to_owned(),
|
display_name: "OpenAI".to_owned(),
|
||||||
models: vec!["gpt-5".to_owned(), "gpt-5-mini".to_owned()],
|
models: vec!["gpt-5".to_owned(), "gpt-5-mini".to_owned()],
|
||||||
|
npm: None,
|
||||||
|
api: None,
|
||||||
|
model_labels: BTreeMap::new(),
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -76,7 +94,7 @@ fn static_fallback_catalogue() -> Vec<OpenCodeProviderCatalogEntry> {
|
|||||||
/// **permanent** compatibility shim tied to the upstream bug, not a
|
/// **permanent** compatibility shim tied to the upstream bug, not a
|
||||||
/// workaround for "OpenCode never launched": even on a machine where OpenCode
|
/// workaround for "OpenCode never launched": even on a machine where OpenCode
|
||||||
/// runs regularly, this is the only path that matches its actual output.
|
/// runs regularly, this is the only path that matches its actual output.
|
||||||
fn opencode_models_cache_path() -> Option<PathBuf> {
|
pub fn opencode_models_cache_path() -> Option<PathBuf> {
|
||||||
let cache_home = std::env::var_os("XDG_CACHE_HOME")
|
let cache_home = std::env::var_os("XDG_CACHE_HOME")
|
||||||
.map(PathBuf::from)
|
.map(PathBuf::from)
|
||||||
.or_else(|| dirs::home_dir().map(|home| home.join(".cache")))?;
|
.or_else(|| dirs::home_dir().map(|home| home.join(".cache")))?;
|
||||||
@ -89,14 +107,18 @@ struct ModelsDevProvider {
|
|||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
name: Option<String>,
|
name: Option<String>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
models: BTreeMap<String, ModelsDevModelIgnored>,
|
npm: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
api: Option<String>,
|
||||||
|
#[serde(default)]
|
||||||
|
models: BTreeMap<String, ModelsDevModel>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Per-model payload: only the key (model id) is used, so the value is parsed
|
|
||||||
/// but its fields are ignored (`deny_unknown_fields` is deliberately absent —
|
|
||||||
/// the cache carries many fields we don't need, e.g. pricing, limits).
|
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
struct ModelsDevModelIgnored {}
|
struct ModelsDevModel {
|
||||||
|
#[serde(default)]
|
||||||
|
name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
/// Parses a models.dev-shaped cache payload into catalogue entries.
|
/// Parses a models.dev-shaped cache payload into catalogue entries.
|
||||||
///
|
///
|
||||||
@ -109,6 +131,17 @@ fn parse_models_dev_cache(bytes: &[u8]) -> Option<Vec<OpenCodeProviderCatalogEnt
|
|||||||
let mut entries: Vec<OpenCodeProviderCatalogEntry> = providers
|
let mut entries: Vec<OpenCodeProviderCatalogEntry> = providers
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|(provider_id, provider)| {
|
.filter_map(|(provider_id, provider)| {
|
||||||
|
let model_labels = provider
|
||||||
|
.models
|
||||||
|
.iter()
|
||||||
|
.filter_map(|(id, model)| {
|
||||||
|
model
|
||||||
|
.name
|
||||||
|
.as_ref()
|
||||||
|
.filter(|name| !name.trim().is_empty())
|
||||||
|
.map(|name| (id.clone(), name.clone()))
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
let models: Vec<String> = provider.models.into_keys().collect();
|
let models: Vec<String> = provider.models.into_keys().collect();
|
||||||
if models.is_empty() {
|
if models.is_empty() {
|
||||||
return None;
|
return None;
|
||||||
@ -121,6 +154,9 @@ fn parse_models_dev_cache(bytes: &[u8]) -> Option<Vec<OpenCodeProviderCatalogEnt
|
|||||||
provider_id,
|
provider_id,
|
||||||
display_name,
|
display_name,
|
||||||
models,
|
models,
|
||||||
|
npm: provider.npm,
|
||||||
|
api: provider.api,
|
||||||
|
model_labels,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
@ -152,6 +188,68 @@ pub fn opencode_provider_catalogue() -> Vec<OpenCodeProviderCatalogEntry> {
|
|||||||
catalogue_from_cache_bytes(bytes)
|
catalogue_from_cache_bytes(bytes)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Builds the persisted standalone-provider metadata for a catalogue provider.
|
||||||
|
///
|
||||||
|
/// OpenCode only knows the three allowlisted providers without a registry
|
||||||
|
/// cache. Every other provider must therefore carry enough configuration to
|
||||||
|
/// remain runnable after the host cache disappears.
|
||||||
|
pub(crate) fn catalogue_custom_provider(
|
||||||
|
provider_id: &str,
|
||||||
|
model_id: &str,
|
||||||
|
) -> Result<Option<CustomProviderConfig>, String> {
|
||||||
|
catalogue_custom_provider_from(opencode_provider_catalogue(), provider_id, model_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn catalogue_custom_provider_from(
|
||||||
|
catalogue: Vec<OpenCodeProviderCatalogEntry>,
|
||||||
|
provider_id: &str,
|
||||||
|
model_id: &str,
|
||||||
|
) -> Result<Option<CustomProviderConfig>, String> {
|
||||||
|
if BUILT_IN_PROVIDERS.contains(&provider_id) {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
|
||||||
|
let provider = catalogue
|
||||||
|
.into_iter()
|
||||||
|
.find(|entry| entry.provider_id == provider_id)
|
||||||
|
.ok_or_else(|| format!("provider `{provider_id}` is absent from models.dev"))?;
|
||||||
|
if !provider.models.iter().any(|model| model == model_id) {
|
||||||
|
return Err(format!(
|
||||||
|
"model `{model_id}` is absent from models.dev provider `{provider_id}`"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
let npm = required_metadata(provider.npm, "npm", provider_id)?;
|
||||||
|
let api = required_metadata(provider.api, "api", provider_id)?;
|
||||||
|
if !(api.starts_with("http://") || api.starts_with("https://")) {
|
||||||
|
return Err(format!(
|
||||||
|
"models.dev provider `{provider_id}` has an invalid api URL"
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let label = provider
|
||||||
|
.model_labels
|
||||||
|
.get(model_id)
|
||||||
|
.cloned()
|
||||||
|
.filter(|label| !label.trim().is_empty())
|
||||||
|
.ok_or_else(|| {
|
||||||
|
format!("model `{model_id}` of models.dev provider `{provider_id}` has no label")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
CustomProviderConfig::new(npm, api, Some(label))
|
||||||
|
.map(Some)
|
||||||
|
.map_err(|error| error.to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn required_metadata(
|
||||||
|
value: Option<String>,
|
||||||
|
field: &str,
|
||||||
|
provider_id: &str,
|
||||||
|
) -> Result<String, String> {
|
||||||
|
value
|
||||||
|
.filter(|value| !value.trim().is_empty())
|
||||||
|
.ok_or_else(|| format!("models.dev provider `{provider_id}` has no valid `{field}`"))
|
||||||
|
}
|
||||||
|
|
||||||
/// Use case exposing [`opencode_provider_catalogue`] to the driving side. No
|
/// Use case exposing [`opencode_provider_catalogue`] to the driving side. No
|
||||||
/// port: reading the OpenCode cache is a best-effort local convenience read,
|
/// port: reading the OpenCode cache is a best-effort local convenience read,
|
||||||
/// never a failure mode the driving side needs to react to (see the
|
/// never a failure mode the driving side needs to react to (see the
|
||||||
@ -277,6 +375,73 @@ mod tests {
|
|||||||
assert_eq!(models, vec!["gpt-5".to_owned(), "gpt-5-mini".to_owned()]);
|
assert_eq!(models, vec!["gpt-5".to_owned(), "gpt-5-mini".to_owned()]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn models_dev_parser_keeps_runtime_metadata_and_model_labels() {
|
||||||
|
let json = br#"{
|
||||||
|
"zai": {
|
||||||
|
"name": "Z.AI",
|
||||||
|
"npm": "@ai-sdk/openai-compatible",
|
||||||
|
"api": "https://api.z.ai/api/paas/v4",
|
||||||
|
"models": {
|
||||||
|
"glm-5.1": {"name": "GLM-5.1"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"#;
|
||||||
|
let entries = parse_models_dev_cache(json).unwrap();
|
||||||
|
let zai = &entries[0];
|
||||||
|
assert_eq!(zai.npm.as_deref(), Some("@ai-sdk/openai-compatible"));
|
||||||
|
assert_eq!(zai.api.as_deref(), Some("https://api.z.ai/api/paas/v4"));
|
||||||
|
assert_eq!(
|
||||||
|
zai.model_labels.get("glm-5.1").map(String::as_str),
|
||||||
|
Some("GLM-5.1")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zai_is_materialized_as_a_standalone_provider() {
|
||||||
|
let catalogue = parse_models_dev_cache(
|
||||||
|
br#"{"zai":{"npm":"@ai-sdk/openai-compatible","api":"https://api.z.ai/api/paas/v4","models":{"glm-5.1":{"name":"GLM-5.1"}}}}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let custom = catalogue_custom_provider_from(catalogue, "zai", "glm-5.1")
|
||||||
|
.unwrap()
|
||||||
|
.unwrap();
|
||||||
|
assert_eq!(custom.npm, "@ai-sdk/openai-compatible");
|
||||||
|
assert_eq!(custom.base_url, "https://api.z.ai/api/paas/v4");
|
||||||
|
assert_eq!(custom.display_name.as_deref(), Some("GLM-5.1"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn built_ins_do_not_require_models_dev_runtime_metadata() {
|
||||||
|
assert_eq!(
|
||||||
|
catalogue_custom_provider_from(Vec::new(), "anthropic", "claude-sonnet-5").unwrap(),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn standalone_provider_rejects_missing_or_invalid_runtime_metadata() {
|
||||||
|
let missing_label = parse_models_dev_cache(
|
||||||
|
br#"{"zai":{"npm":"@ai-sdk/openai-compatible","api":"https://api.z.ai/api/paas/v4","models":{"glm-5.1":{}}}}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
catalogue_custom_provider_from(missing_label, "zai", "glm-5.1")
|
||||||
|
.unwrap_err()
|
||||||
|
.contains("has no label")
|
||||||
|
);
|
||||||
|
|
||||||
|
let invalid_api = parse_models_dev_cache(
|
||||||
|
br#"{"zai":{"npm":"@ai-sdk/openai-compatible","api":"api.z.ai","models":{"glm-5.1":{"name":"GLM-5.1"}}}}"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
assert!(
|
||||||
|
catalogue_custom_provider_from(invalid_api, "zai", "glm-5.1")
|
||||||
|
.unwrap_err()
|
||||||
|
.contains("invalid api URL")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn catalogue_from_cache_bytes_defaults_display_name_to_provider_id() {
|
fn catalogue_from_cache_bytes_defaults_display_name_to_provider_id() {
|
||||||
let json = br#"{"mystery": {"models": {"m1": {}}}}"#.to_vec();
|
let json = br#"{"mystery": {"models": {"m1": {}}}}"#.to_vec();
|
||||||
|
|||||||
@ -22,6 +22,7 @@ use domain::profile::{
|
|||||||
use crate::error::AppError;
|
use crate::error::AppError;
|
||||||
|
|
||||||
use super::catalogue::{reference_profile_id, reference_profiles, selectable_reference_profiles};
|
use super::catalogue::{reference_profile_id, reference_profiles, selectable_reference_profiles};
|
||||||
|
use super::provider_catalogue::catalogue_custom_provider;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// DetectProfiles
|
// DetectProfiles
|
||||||
@ -361,6 +362,12 @@ impl SaveOpenCodeProviderProfile {
|
|||||||
&self,
|
&self,
|
||||||
input: SaveOpenCodeProviderProfileInput,
|
input: SaveOpenCodeProviderProfileInput,
|
||||||
) -> Result<SaveOpenCodeProviderProfileOutput, AppError> {
|
) -> Result<SaveOpenCodeProviderProfileOutput, AppError> {
|
||||||
|
let custom = match input.custom {
|
||||||
|
Some(custom) => Some(custom),
|
||||||
|
None => catalogue_custom_provider(&input.provider_id, &input.model)
|
||||||
|
.map_err(AppError::Invalid)?,
|
||||||
|
};
|
||||||
|
|
||||||
let secret_ref = input
|
let secret_ref = input
|
||||||
.profile
|
.profile
|
||||||
.opencode_provider
|
.opencode_provider
|
||||||
@ -370,10 +377,9 @@ impl SaveOpenCodeProviderProfile {
|
|||||||
|
|
||||||
self.secret_store.put(&secret_ref, &input.api_key).await?;
|
self.secret_store.put(&secret_ref, &input.api_key).await?;
|
||||||
|
|
||||||
let mut provider =
|
let mut provider = OpenCodeProviderConfig::new(input.provider_id, input.model, secret_ref)
|
||||||
OpenCodeProviderConfig::new(input.provider_id, input.model, secret_ref)
|
.map_err(|e| AppError::Invalid(e.to_string()))?;
|
||||||
.map_err(|e| AppError::Invalid(e.to_string()))?;
|
if let Some(custom) = custom {
|
||||||
if let Some(custom) = input.custom {
|
|
||||||
provider = provider.with_custom(custom);
|
provider = provider.with_custom(custom);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -611,18 +611,14 @@ fn parent_dir(base: &ProjectPath, rel: &str) -> String {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod opencode_provider_config_json_tests {
|
mod opencode_provider_config_json_tests {
|
||||||
use domain::profile::CustomProviderConfig;
|
|
||||||
use domain::ports::SecretRef;
|
use domain::ports::SecretRef;
|
||||||
|
use domain::profile::CustomProviderConfig;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn known_provider_config() -> OpenCodeProviderConfig {
|
fn known_provider_config() -> OpenCodeProviderConfig {
|
||||||
OpenCodeProviderConfig::new(
|
OpenCodeProviderConfig::new("anthropic", "claude-sonnet-5", SecretRef::new("secret-ref"))
|
||||||
"anthropic",
|
.unwrap()
|
||||||
"claude-sonnet-5",
|
|
||||||
SecretRef::new("secret-ref"),
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn custom_provider_config() -> OpenCodeProviderConfig {
|
fn custom_provider_config() -> OpenCodeProviderConfig {
|
||||||
@ -674,6 +670,30 @@ mod opencode_provider_config_json_tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn zai_provider_is_rendered_as_a_cache_independent_provider() {
|
||||||
|
let custom = CustomProviderConfig::new(
|
||||||
|
"@ai-sdk/openai-compatible",
|
||||||
|
"https://api.z.ai/api/paas/v4",
|
||||||
|
Some("GLM-5.1".to_owned()),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let config = OpenCodeProviderConfig::new("zai", "glm-5.1", SecretRef::new("ref"))
|
||||||
|
.unwrap()
|
||||||
|
.with_custom(custom);
|
||||||
|
let body = opencode_provider_config_json(&config, "secret", "/project", None, None);
|
||||||
|
assert_eq!(body["model"], "zai/glm-5.1");
|
||||||
|
assert_eq!(body["provider"]["zai"]["npm"], "@ai-sdk/openai-compatible");
|
||||||
|
assert_eq!(
|
||||||
|
body["provider"]["zai"]["options"]["baseURL"],
|
||||||
|
"https://api.z.ai/api/paas/v4"
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
body["provider"]["zai"]["models"]["glm-5.1"]["name"],
|
||||||
|
"GLM-5.1"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- permission projection wiring (ticket #94) -----------------------
|
// ---- permission projection wiring (ticket #94) -----------------------
|
||||||
|
|
||||||
/// Builds an [`EffectivePermissions`] with the given fallback posture (only
|
/// Builds an [`EffectivePermissions`] with the given fallback posture (only
|
||||||
|
|||||||
Reference in New Issue
Block a user