Skip to main content

rig_core/providers/copilot/
wire.rs

1//! Copilot's dialect of the OpenAI wires. Completion selects the
2//! Responses route for Codex model identifiers and Chat Completions
3//! otherwise; every request carries the editor envelope. Credentials must be
4//! exchanged before encoding (see [`auth`](super::auth)).
5//!
6//! ```no_run
7//! use rig_core::providers::copilot::{self, DIALECT};
8//! use rig_core::providers::openai::OpenAIConfig;
9//!
10//! # fn run(http: rig_core::http_client::DynHttpClient) {
11//! let github = OpenAIConfig::with_key(&DIALECT, "tid=session-token").connect(http);
12//! let model = github.completion(copilot::GPT_4O);
13//! # let _ = model;
14//! # }
15//! ```
16
17use crate::completion::CompletionRequest;
18use crate::providers::openai::responses_api::SystemInstructionsPlacement;
19use crate::providers::openai::wire::{
20    Dialect, DialectHooks, EmbeddingQuirks, OpenAIConfig, Quirks, ResponsesQuirks, Route,
21};
22use crate::wire::Body;
23
24use super::PROVIDER_NAME;
25
26/// The reply header carrying Copilot's transport request id. Copilot relays
27/// OpenAI's wire on both routes, header included.
28pub(super) const REQUEST_ID_HEADER: Option<&str> = Some("x-request-id");
29
30/// Copilot dialect with model-based routing and editor-identity headers.
31/// Responses requests place system messages in `input`. Verification uses
32/// token exchange rather than a dedicated endpoint.
33pub const DIALECT: Dialect = Dialect {
34    base_url_env: Some("GITHUB_COPILOT_API_BASE"),
35    request_id_header: REQUEST_ID_HEADER,
36    quirks: Quirks {
37        hooks: Some(&HOOKS),
38        verify_path: "",
39        // Copilot bills a subscription, not the catalog's API rates.
40        catalog_priced: false,
41        base_url_env_alias: Some("COPILOT_BASE_URL"),
42        // Copilot keeps no files, so no file id resolves there.
43        accepts_file_ids: false,
44        short_lived_key: true,
45        embedding: EmbeddingQuirks {
46            requires_usage: false,
47            ..EmbeddingQuirks::openai()
48        },
49        responses: ResponsesQuirks {
50            strict_tools_by_default: true,
51            system_instructions: SystemInstructionsPlacement::InputSystemMessages,
52            ..ResponsesQuirks::openai()
53        },
54        ..Quirks::openai()
55    },
56    ..Dialect::gateway(
57        PROVIDER_NAME,
58        super::GITHUB_COPILOT_API_BASE_URL,
59        "GITHUB_COPILOT_API_KEY",
60    )
61};
62
63static HOOKS: DialectHooks = DialectHooks {
64    default_endpoint: Some(super::auth::base_url_from_token),
65    model_route: Some(|model| {
66        if routes_through_responses(model) {
67            Route::Responses
68        } else {
69            Route::Chat
70        }
71    }),
72    completion_envelope: Some(completion_envelope),
73    // Non-conversational modality requests use a user initiator.
74    modality_envelope: Some(|provider, request| {
75        stamp(request, provider.api_key.expose(), "user", false)
76    }),
77};
78
79/// The editor envelope of a completion request: the initiator and vision
80/// flag follow the request's history.
81fn completion_envelope(
82    provider: &OpenAIConfig,
83    request: &CompletionRequest,
84    mut builder: http::request::Builder,
85) -> http::request::Builder {
86    for (name, value) in super::default_headers(
87        provider.api_key.expose(),
88        super::request_initiator(request),
89        super::request_has_vision(request),
90    ) {
91        if let Some(headers) = builder.headers_mut() {
92            headers.remove(name);
93        }
94        builder = builder.header(name, value);
95    }
96    builder
97}
98
99/// Return whether `model` contains `codex`, case-insensitively, selecting `/responses`.
100pub fn routes_through_responses(model: &str) -> bool {
101    model.to_ascii_lowercase().contains("codex")
102}
103
104/// Stamp Copilot's request envelope onto a built request.
105///
106/// Used by the modality hook and the catalogue wire. `insert` replaces the
107/// shared authentication header rather than appending a second credential.
108/// Completion routes use `completion_envelope` during encoding instead.
109pub(super) fn stamp(
110    request: &mut http::Request<Body>,
111    api_key: &str,
112    initiator: &'static str,
113    has_vision: bool,
114) -> Result<(), http::Error> {
115    let map = request.headers_mut();
116    for (name, value) in super::default_headers(api_key, initiator, has_vision) {
117        map.insert(
118            http::HeaderName::from_bytes(name.as_bytes())?,
119            http::HeaderValue::from_str(&value)?,
120        );
121    }
122    Ok(())
123}
124
125#[cfg(test)]
126mod tests;