use serde::{Deserialize, Serialize};
use crate::client::env::{self, EnvError};
use crate::completion::CompletionRequest;
use crate::error::EncodeError;
use crate::operation::Completion;
use crate::providers::openai::responses_api::SystemInstructionsPlacement;
pub use crate::providers::openai::wire::Embeddings;
use crate::providers::openai::wire::{
Dialect, DialectHooks, EmbeddingQuirks, OpenAIConfig, OpenAiDecoder, OpenAiReassembler,
OpenAiWire, Quirks, ResponsesQuirks, Route,
};
use crate::wire::{Body, Descriptor, Encoded, Mode, Secret, Wire};
use super::{CopilotIntent, PROVIDER_NAME};
pub(super) const REQUEST_ID_HEADER: Option<&str> = Some("x-request-id");
const PRIMARY_API_KEY_ENV: &str = "GITHUB_COPILOT_API_KEY";
const API_KEY_ENV: [&str; 2] = ["GITHUB_COPILOT_API_KEY", "COPILOT_API_KEY"];
const BASE_URL_ENV: &[&str] = &["GITHUB_COPILOT_API_BASE", "COPILOT_BASE_URL"];
pub const DIALECT: Dialect = Dialect {
base_url_env: Some("GITHUB_COPILOT_API_BASE"),
request_id_header: REQUEST_ID_HEADER,
quirks: Quirks {
hooks: Some(&HOOKS),
verify_path: "",
base_url_env_alias: Some("COPILOT_BASE_URL"),
accepts_file_ids: false,
embedding: EmbeddingQuirks {
requires_usage: false,
..EmbeddingQuirks::openai()
},
responses: ResponsesQuirks {
strict_tools_by_default: true,
system_instructions: SystemInstructionsPlacement::InputSystemMessages,
..ResponsesQuirks::openai()
},
..Quirks::openai()
},
..Dialect::gateway(
PROVIDER_NAME,
super::GITHUB_COPILOT_API_BASE_URL,
"GITHUB_COPILOT_API_KEY",
)
};
static HOOKS: DialectHooks = DialectHooks {
default_endpoint: Some(super::auth::base_url_from_token),
model_route: Some(|model| {
if routes_through_responses(model) {
Route::Responses
} else {
Route::Chat
}
}),
completion_envelope: Some(|provider, request, builder| {
completion_envelope(provider, request, builder, CopilotIntent::default())
}),
modality_envelope: Some(|provider, request| {
stamp(
request,
provider.api_key.expose(),
"user",
false,
CopilotIntent::Panel,
)
}),
};
fn completion_envelope(
provider: &OpenAIConfig,
request: &CompletionRequest,
mut builder: http::request::Builder,
intent: CopilotIntent,
) -> http::request::Builder {
for (name, value) in super::default_headers(
provider.api_key.expose(),
super::request_initiator(request),
super::request_has_vision(request),
intent,
) {
if let Some(headers) = builder.headers_mut() {
headers.remove(name);
}
builder = builder.header(name, value);
}
builder
}
pub fn routes_through_responses(model: &str) -> bool {
model.to_ascii_lowercase().contains("codex")
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CopilotConfig {
pub api_key: Secret,
pub base_url: String,
}
impl CopilotConfig {
pub fn new(api_key: impl Into<Secret>) -> Self {
let provider = OpenAIConfig::with_key(&DIALECT, api_key);
Self {
api_key: provider.api_key,
base_url: provider.base_url,
}
}
pub fn from_auth(context: &super::auth::AuthContext) -> Self {
let mut provider = Self::new(context.api_key.clone());
if let Some(api_base) = &context.api_base {
provider.base_url = api_base.clone();
}
provider
}
pub fn from_env() -> Result<Self, EnvError> {
let Some(api_key) = first_env(&API_KEY_ENV)? else {
return Err(EnvError::Variable {
name: PRIMARY_API_KEY_ENV,
source: std::env::VarError::NotPresent,
});
};
let mut provider = Self::new(api_key);
if let Some(base_url) = first_env(BASE_URL_ENV)? {
provider.base_url = base_url;
}
Ok(provider)
}
pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
self.base_url = base_url.into();
self
}
pub(crate) fn completion(&self, model: impl Into<String>) -> CopilotWire {
CopilotWire {
wire: self.openai().completion(model),
intent: CopilotIntent::default(),
}
}
pub(crate) fn embedding(&self, model: impl Into<String>, ndims: Option<usize>) -> Embeddings {
Embeddings::new(self.openai(), model, ndims)
}
fn openai(&self) -> OpenAIConfig {
OpenAIConfig::with_key(&DIALECT, self.api_key.clone()).with_base_url(self.base_url.clone())
}
pub(super) fn uri(&self, path: &str) -> String {
format!("{}{path}", self.base_url.trim_end_matches('/'))
}
}
fn first_env(names: &[&'static str]) -> Result<Option<String>, EnvError> {
for name in names {
if let Some(value) = env::optional(name)?.filter(|value| !value.trim().is_empty()) {
return Ok(Some(value));
}
}
Ok(None)
}
pub(super) fn stamp(
request: &mut http::Request<Body>,
api_key: &str,
initiator: &'static str,
has_vision: bool,
intent: CopilotIntent,
) -> Result<(), http::Error> {
let map = request.headers_mut();
for (name, value) in super::default_headers(api_key, initiator, has_vision, intent) {
map.insert(
http::HeaderName::from_bytes(name.as_bytes())?,
http::HeaderValue::from_str(&value)?,
);
}
Ok(())
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub struct CopilotWire {
pub wire: OpenAiWire,
pub intent: CopilotIntent,
}
impl CopilotWire {
pub fn intent(&self) -> CopilotIntent {
self.intent
}
pub fn with_intent(mut self, intent: CopilotIntent) -> Self {
self.intent = intent;
self
}
pub fn with_panel_intent(self) -> Self {
self.with_intent(CopilotIntent::Panel)
}
pub fn with_edits_intent(self) -> Self {
self.with_intent(CopilotIntent::Edits)
}
pub fn with_strict_tools(mut self) -> Self {
self.wire = self.wire.with_strict_tools();
self
}
pub fn with_tool_result_array_content(mut self) -> Self {
if let OpenAiWire::Chat(wire) = self.wire {
self.wire = OpenAiWire::Chat(wire.with_tool_result_array_content());
}
self
}
}
impl Wire for CopilotWire {
type Op = Completion;
type Payload = crate::wire::Encoded;
type Frame = crate::wire::WireFrame;
type Decoder<'id> = OpenAiDecoder;
type Reassembler = OpenAiReassembler;
fn describe(&self) -> Descriptor<'_> {
self.wire.describe()
}
fn encode(&self, request: CompletionRequest, mode: Mode) -> Result<Encoded, EncodeError> {
self.wire
.encode_with_headers(request, mode, |provider, request, builder| {
completion_envelope(provider, request, provider.headers(builder), self.intent)
})
}
fn decoder<'id>(&self) -> Self::Decoder<'id> {
self.wire.decoder()
}
fn reassembler(&self) -> Self::Reassembler {
self.wire.reassembler()
}
}
#[cfg(test)]
mod tests;