systemprompt_models/bridge/opencode/
mod.rs1pub mod agents_md;
19mod policy;
20
21use std::collections::BTreeMap;
22
23use serde::{Deserialize, Serialize};
24
25pub use policy::{Lockdown, Policy, PolicyAction, PolicyEffect, lockdown_policies};
26
27pub const SCHEMA_URL: &str = "https://opencode.ai/config.json";
28
29pub const OPENAI_COMPATIBLE_PACKAGE: &str = "aisdk:@ai-sdk/openai-compatible";
30
31pub const MIN_CLIENT_VERSION: semver::Version = semver::Version::new(2, 0, 30);
32
33#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
35pub struct OpenCodeConfig {
36 #[serde(rename = "$schema")]
37 pub schema: String,
38 #[serde(default, skip_serializing_if = "Option::is_none")]
39 pub model: Option<String>,
40 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub update: Option<String>,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub share: Option<String>,
44 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
45 pub providers: BTreeMap<String, Provider>,
46 #[serde(default, skip_serializing_if = "Vec::is_empty")]
47 pub policies: Vec<Policy>,
48 #[serde(default, skip_serializing_if = "McpBlock::is_empty")]
49 pub mcp: McpBlock,
50}
51
52impl Default for OpenCodeConfig {
53 fn default() -> Self {
54 Self {
55 schema: SCHEMA_URL.to_owned(),
56 model: None,
57 update: Some("disable".to_owned()),
58 share: Some("disabled".to_owned()),
59 providers: BTreeMap::new(),
60 policies: Vec::new(),
61 mcp: McpBlock::default(),
62 }
63 }
64}
65
66impl OpenCodeConfig {
67 #[must_use]
68 pub fn with_provider(mut self, id: &str, provider: Provider) -> Self {
69 self.providers.insert(id.to_owned(), provider);
70 self
71 }
72
73 #[must_use]
74 pub fn with_model(mut self, provider: &str, alias: &str) -> Self {
75 self.model = Some(format!("{provider}/{alias}"));
76 self
77 }
78
79 #[must_use]
80 pub fn with_policies(mut self, policies: Vec<Policy>) -> Self {
81 self.policies = policies;
82 self
83 }
84
85 #[must_use]
86 pub fn with_mcp_server(mut self, name: &str, server: McpServer) -> Self {
87 self.mcp.servers.insert(name.to_owned(), server);
88 self
89 }
90
91 pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
92 serde_json::to_string_pretty(self).map(|mut text| {
93 text.push('\n');
94 text
95 })
96 }
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
101pub struct Provider {
102 pub name: String,
103 pub package: String,
104 pub settings: ProviderSettings,
105 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
106 pub models: BTreeMap<String, ModelEntry>,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
110pub struct ProviderSettings {
111 #[serde(rename = "baseURL")]
112 pub base_url: String,
113 #[serde(rename = "apiKey")]
114 pub api_key: String,
115 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
116 pub headers: BTreeMap<String, String>,
117}
118
119#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
120pub struct ModelEntry {
121 #[serde(rename = "modelID")]
122 pub model: String,
123 #[serde(default, skip_serializing_if = "Option::is_none")]
124 pub name: Option<String>,
125 #[serde(default, skip_serializing_if = "Option::is_none")]
126 pub limit: Option<ModelLimit>,
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
130pub struct ModelLimit {
131 pub context: u64,
132 pub output: u64,
133}
134
135#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
136pub struct McpBlock {
137 #[serde(default)]
138 pub servers: BTreeMap<String, McpServer>,
139}
140
141impl McpBlock {
142 #[must_use]
143 pub fn is_empty(&self) -> bool {
144 self.servers.is_empty()
145 }
146}
147
148#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
150#[serde(tag = "type", rename_all = "snake_case")]
151pub enum McpServer {
152 Remote {
153 url: String,
154 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
155 headers: BTreeMap<String, String>,
156 #[serde(default, skip_serializing_if = "Option::is_none")]
157 codemode: Option<bool>,
158 #[serde(default, skip_serializing_if = "Option::is_none")]
159 oauth: Option<McpOAuth>,
160 },
161 Local {
162 command: Vec<String>,
163 #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
164 environment: BTreeMap<String, String>,
165 },
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
172pub enum McpOAuth {
173 Disabled,
174}
175
176impl Serialize for McpOAuth {
177 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
178 serializer.serialize_bool(false)
179 }
180}
181
182impl<'de> Deserialize<'de> for McpOAuth {
183 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
184 if bool::deserialize(deserializer)? {
185 return Err(serde::de::Error::custom(
186 "only `oauth: false` is modelled for a remote MCP server",
187 ));
188 }
189 Ok(Self::Disabled)
190 }
191}
192
193#[derive(Debug, Clone, PartialEq, Eq)]
195pub enum ApiKeyRef {
196 Env(String),
197 File(String),
198}
199
200impl ApiKeyRef {
201 #[must_use]
202 pub fn render(&self) -> String {
203 match self {
204 Self::Env(var) => format!("{{env:{var}}}"),
205 Self::File(path) => format!("{{file:{path}}}"),
206 }
207 }
208}
209
210#[must_use]
211pub fn gateway_provider(
212 base_url: &str,
213 api_key: &ApiKeyRef,
214 headers: BTreeMap<String, String>,
215 models: BTreeMap<String, ModelEntry>,
216) -> Provider {
217 Provider {
218 name: "systemprompt".to_owned(),
219 package: OPENAI_COMPATIBLE_PACKAGE.to_owned(),
220 settings: ProviderSettings {
221 base_url: format!("{}/v1", base_url.trim_end_matches('/')),
222 api_key: api_key.render(),
223 headers,
224 },
225 models,
226 }
227}