rig_core/providers/copilot/
wire.rs1use serde::{Deserialize, Serialize};
16
17use crate::client::env::{self, EnvError};
18use crate::completion::CompletionRequest;
19use crate::error::EncodeError;
20use crate::operation::Completion;
21use crate::providers::openai::responses_api::SystemInstructionsPlacement;
22pub use crate::providers::openai::wire::Embeddings;
26use crate::providers::openai::wire::{
27 Dialect, DialectHooks, EmbeddingQuirks, OpenAIConfig, OpenAiDecoder, OpenAiReassembler,
28 OpenAiWire, Quirks, ResponsesQuirks, Route,
29};
30use crate::wire::{Body, Descriptor, Encoded, Mode, Secret, Wire};
31
32use super::{CopilotIntent, PROVIDER_NAME};
33
34pub(super) const REQUEST_ID_HEADER: Option<&str> = Some("x-request-id");
37
38const PRIMARY_API_KEY_ENV: &str = "GITHUB_COPILOT_API_KEY";
40
41const API_KEY_ENV: [&str; 2] = ["GITHUB_COPILOT_API_KEY", "COPILOT_API_KEY"];
43
44const BASE_URL_ENV: &[&str] = &["GITHUB_COPILOT_API_BASE", "COPILOT_BASE_URL"];
46
47pub const DIALECT: Dialect = Dialect {
51 base_url_env: Some("GITHUB_COPILOT_API_BASE"),
52 request_id_header: REQUEST_ID_HEADER,
53 quirks: Quirks {
54 hooks: Some(&HOOKS),
55 verify_path: "",
56 base_url_env_alias: Some("COPILOT_BASE_URL"),
57 accepts_file_ids: false,
59 embedding: EmbeddingQuirks {
60 requires_usage: false,
61 ..EmbeddingQuirks::openai()
62 },
63 responses: ResponsesQuirks {
64 strict_tools_by_default: true,
65 system_instructions: SystemInstructionsPlacement::InputSystemMessages,
66 ..ResponsesQuirks::openai()
67 },
68 ..Quirks::openai()
69 },
70 ..Dialect::gateway(
71 PROVIDER_NAME,
72 super::GITHUB_COPILOT_API_BASE_URL,
73 "GITHUB_COPILOT_API_KEY",
74 )
75};
76
77static HOOKS: DialectHooks = DialectHooks {
78 default_endpoint: Some(super::auth::base_url_from_token),
79 model_route: Some(|model| {
80 if routes_through_responses(model) {
81 Route::Responses
82 } else {
83 Route::Chat
84 }
85 }),
86 completion_envelope: Some(|provider, request, builder| {
87 completion_envelope(provider, request, builder, CopilotIntent::default())
88 }),
89 modality_envelope: Some(|provider, request| {
91 stamp(
92 request,
93 provider.api_key.expose(),
94 "user",
95 false,
96 CopilotIntent::Panel,
97 )
98 }),
99};
100
101fn completion_envelope(
103 provider: &OpenAIConfig,
104 request: &CompletionRequest,
105 mut builder: http::request::Builder,
106 intent: CopilotIntent,
107) -> http::request::Builder {
108 for (name, value) in super::default_headers(
109 provider.api_key.expose(),
110 super::request_initiator(request),
111 super::request_has_vision(request),
112 intent,
113 ) {
114 if let Some(headers) = builder.headers_mut() {
115 headers.remove(name);
116 }
117 builder = builder.header(name, value);
118 }
119 builder
120}
121
122pub fn routes_through_responses(model: &str) -> bool {
124 model.to_ascii_lowercase().contains("codex")
125}
126
127#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
132pub struct CopilotConfig {
133 pub api_key: Secret,
135 pub base_url: String,
137}
138
139impl CopilotConfig {
140 pub fn new(api_key: impl Into<Secret>) -> Self {
144 let provider = OpenAIConfig::with_key(&DIALECT, api_key);
145 Self {
146 api_key: provider.api_key,
147 base_url: provider.base_url,
148 }
149 }
150
151 pub fn from_auth(context: &super::auth::AuthContext) -> Self {
154 let mut provider = Self::new(context.api_key.clone());
155 if let Some(api_base) = &context.api_base {
156 provider.base_url = api_base.clone();
157 }
158 provider
159 }
160
161 pub fn from_env() -> Result<Self, EnvError> {
166 let Some(api_key) = first_env(&API_KEY_ENV)? else {
167 return Err(EnvError::Variable {
168 name: PRIMARY_API_KEY_ENV,
169 source: std::env::VarError::NotPresent,
170 });
171 };
172 let mut provider = Self::new(api_key);
173 if let Some(base_url) = first_env(BASE_URL_ENV)? {
174 provider.base_url = base_url;
175 }
176 Ok(provider)
177 }
178
179 pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
181 self.base_url = base_url.into();
182 self
183 }
184
185 pub(crate) fn completion(&self, model: impl Into<String>) -> CopilotWire {
187 CopilotWire {
188 wire: self.openai().completion(model),
189 intent: CopilotIntent::default(),
190 }
191 }
192
193 pub(crate) fn embedding(&self, model: impl Into<String>, ndims: Option<usize>) -> Embeddings {
196 Embeddings::new(self.openai(), model, ndims)
197 }
198
199 fn openai(&self) -> OpenAIConfig {
201 OpenAIConfig::with_key(&DIALECT, self.api_key.clone()).with_base_url(self.base_url.clone())
202 }
203
204 pub(super) fn uri(&self, path: &str) -> String {
206 format!("{}{path}", self.base_url.trim_end_matches('/'))
207 }
208}
209
210fn first_env(names: &[&'static str]) -> Result<Option<String>, EnvError> {
213 for name in names {
214 if let Some(value) = env::optional(name)?.filter(|value| !value.trim().is_empty()) {
215 return Ok(Some(value));
216 }
217 }
218 Ok(None)
219}
220
221pub(super) fn stamp(
227 request: &mut http::Request<Body>,
228 api_key: &str,
229 initiator: &'static str,
230 has_vision: bool,
231 intent: CopilotIntent,
232) -> Result<(), http::Error> {
233 let map = request.headers_mut();
234 for (name, value) in super::default_headers(api_key, initiator, has_vision, intent) {
235 map.insert(
236 http::HeaderName::from_bytes(name.as_bytes())?,
237 http::HeaderValue::from_str(&value)?,
238 );
239 }
240 Ok(())
241}
242
243#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
247pub struct CopilotWire {
248 pub wire: OpenAiWire,
250 pub intent: CopilotIntent,
252}
253
254impl CopilotWire {
255 pub fn intent(&self) -> CopilotIntent {
257 self.intent
258 }
259
260 pub fn with_intent(mut self, intent: CopilotIntent) -> Self {
262 self.intent = intent;
263 self
264 }
265
266 pub fn with_panel_intent(self) -> Self {
268 self.with_intent(CopilotIntent::Panel)
269 }
270
271 pub fn with_edits_intent(self) -> Self {
273 self.with_intent(CopilotIntent::Edits)
274 }
275
276 pub fn with_strict_tools(mut self) -> Self {
281 self.wire = self.wire.with_strict_tools();
282 self
283 }
284
285 pub fn with_tool_result_array_content(mut self) -> Self {
290 if let OpenAiWire::Chat(wire) = self.wire {
291 self.wire = OpenAiWire::Chat(wire.with_tool_result_array_content());
292 }
293 self
294 }
295}
296
297impl Wire for CopilotWire {
298 type Op = Completion;
299 type Payload = crate::wire::Encoded;
300 type Frame = crate::wire::WireFrame;
301 type Decoder<'id> = OpenAiDecoder;
302 type Reassembler = OpenAiReassembler;
303
304 fn describe(&self) -> Descriptor<'_> {
305 self.wire.describe()
306 }
307
308 fn encode(&self, request: CompletionRequest, mode: Mode) -> Result<Encoded, EncodeError> {
309 self.wire
310 .encode_with_headers(request, mode, |provider, request, builder| {
311 completion_envelope(provider, request, provider.headers(builder), self.intent)
312 })
313 }
314
315 fn decoder<'id>(&self) -> Self::Decoder<'id> {
316 self.wire.decoder()
317 }
318
319 fn reassembler(&self) -> Self::Reassembler {
320 self.wire.reassembler()
321 }
322}
323
324#[cfg(test)]
325mod tests;