mirror of
https://github.com/openai/codex.git
synced 2026-10-20 00:18:49 +00:00
Enable GitHub Actions OIDC authentication in the Responses API proxy (#55320)
## Why GitHub Actions workflows can use short-lived OpenAI credentials instead of storing a long-lived OpenAI API key in GitHub secrets. ## What changed - Add `--github-oidc` with required audience, identity provider, service account, and GitHub token-request URL settings. Read the GitHub request credential from stdin and connect it to the existing federated authentication flow. - Require the default OpenAI Responses endpoint in OIDC mode before reading credentials, and complete the initial token exchange before publishing server info. - Close the npm launcher's copy of stdin after the native child starts. - Document OIDC setup, credential handling, renewal behavior, and caller responsibilities. Keep API-key authentication as the default. ## Testing Add tests for required OIDC flags, upstream restrictions before credential reads, rejection of unsafe or empty settings without exposing sensitive input, and failure when a configured custom CA cannot be loaded in the CLI blocking context. GitOrigin-RevId: 7de6fd4c3491e7ccd6a2c2ccdee0bc2eedd0e642
This commit is contained in:
@@ -28,9 +28,13 @@ codex -p proxy
|
||||
|
||||
# Detailed docs
|
||||
|
||||
A strict HTTP proxy that only forwards `POST` requests to `/v1/responses` to the OpenAI API (`https://api.openai.com`), injecting the `Authorization: Bearer $OPENAI_API_KEY` header. Everything else is rejected with `403 Forbidden`.
|
||||
A strict HTTP proxy that forwards `POST /v1/responses` to the OpenAI API
|
||||
(`https://api.openai.com`). It authenticates with an API key by default, or with
|
||||
short-lived OpenAI credentials obtained through GitHub Actions OIDC. Other
|
||||
requests are rejected with `403 Forbidden`, except for the optional shutdown
|
||||
endpoint described below.
|
||||
|
||||
## Expected Usage
|
||||
## API-key usage (default)
|
||||
|
||||
**IMPORTANT:** `codex-responses-api-proxy` is designed to be run by a privileged user with access to `OPENAI_API_KEY` so that an unprivileged user cannot inspect or tamper with the process. Though if `--http-shutdown` is specified, an unprivileged user _can_ make a `GET` request to `/shutdown` to shutdown the server, as an unprivileged user could not send `SIGTERM` to kill the process.
|
||||
|
||||
@@ -56,12 +60,94 @@ When the unprivileged user was finished, they could shutdown the server using `c
|
||||
curl --fail --silent --show-error "${PROXY_BASE_URL}/shutdown"
|
||||
```
|
||||
|
||||
## GitHub Actions OIDC (OpenAI API)
|
||||
|
||||
The proxy can exchange GitHub Actions OIDC identities for short-lived OpenAI
|
||||
API service account credentials. This removes the need to store a long-lived
|
||||
OpenAI API key in GitHub secrets. It requires a workflow/job with
|
||||
`permissions: { id-token: write, contents: read }` and an OpenAI organization
|
||||
owner to configure a workload identity provider and service account mapping.
|
||||
See the [OpenAI GitHub Actions federation guide](https://developers.openai.com/api/docs/guides/workload-identity-federation/github-actions).
|
||||
Set the provider audience to the exact value used by the workflow, and restrict
|
||||
the mapping to trusted repository, ref, and workflow claims.
|
||||
|
||||
In this mode, stdin supplies GitHub's `ACTIONS_ID_TOKEN_REQUEST_TOKEN`, **not**
|
||||
the minted GitHub OIDC JWT or an OpenAI API key. The proxy needs that request
|
||||
credential to request fresh subject tokens during long runs.
|
||||
|
||||
For the npm-installed proxy, use Node.js 24 or newer for the example below.
|
||||
It sets `NODE_OPTIONS=--disable-sigusr1` to protect the launcher's credential
|
||||
input from Node's debugger. Some older Node versions accepted by the npm package
|
||||
do not support this option and will exit before starting the proxy. To run
|
||||
without Node.js, replace `codex-responses-api-proxy` in the example with the
|
||||
path to the native binary; keep the same pipe and environment handling.
|
||||
|
||||
Supply the audience, provider ID, and service account ID as GitHub Actions
|
||||
variables (they are not secrets). Start the proxy as a privileged user, as in
|
||||
the API-key example:
|
||||
|
||||
```bash
|
||||
printenv ACTIONS_ID_TOKEN_REQUEST_TOKEN | (
|
||||
exec env -u ACTIONS_ID_TOKEN_REQUEST_TOKEN -u NODE_OPTIONS \
|
||||
NODE_OPTIONS=--disable-sigusr1 \
|
||||
codex-responses-api-proxy \
|
||||
--github-oidc \
|
||||
--oidc-audience "$OPENAI_WIF_AUDIENCE" \
|
||||
--identity-provider-id "$OPENAI_IDENTITY_PROVIDER_ID" \
|
||||
--service-account-id "$OPENAI_SERVICE_ACCOUNT_ID" \
|
||||
--github-oidc-request-url "$ACTIONS_ID_TOKEN_REQUEST_URL" \
|
||||
--http-shutdown --server-info /tmp/server-info.json
|
||||
)
|
||||
```
|
||||
|
||||
All four OIDC settings are required with `--github-oidc` and are rejected
|
||||
without it. Pass `--github-oidc-request-url` directly from
|
||||
`ACTIONS_ID_TOKEN_REQUEST_URL` supplied by the trusted GitHub runner, as shown
|
||||
above. Do not set or overwrite it from workflow inputs, repository content, or
|
||||
an untrusted subprocess. The proxy sends the GitHub request credential to that
|
||||
URL. It requires HTTPS and refuses redirects, but it does not verify that GitHub
|
||||
owns the destination. Do not substitute an OIDC issuer or discovery URL.
|
||||
|
||||
In OIDC mode only the default `https://api.openai.com/v1/responses` upstream is
|
||||
allowed, and redirects on both token and Responses requests are disabled.
|
||||
Azure/custom provider URLs continue to work in API-key mode.
|
||||
|
||||
Use a real pipe for either credential type, as in the examples above. Bash
|
||||
here-strings and here-documents can create a seekable temporary file containing
|
||||
the credential. If you background the proxy, do not wrap the entire pipeline
|
||||
in another shell: it could retain the original credential environment while
|
||||
it waits.
|
||||
|
||||
The proxy performs an initial exchange before writing server info. It keeps
|
||||
the access token in process memory, renews it using a fresh GitHub JWT before
|
||||
expiry, and serializes concurrent renewals. A failed renewal fails closed with
|
||||
HTTP 502; it does not reuse an expired token or replay a Responses POST. It
|
||||
shares the failure with waiting requests and delays further exchange attempts
|
||||
by one second, doubling after each failure up to 30 seconds. A successful
|
||||
exchange resets that delay. If the Responses endpoint returns 401, the proxy
|
||||
passes through that response and discards only the credential used for it;
|
||||
the next request can obtain a fresh one.
|
||||
|
||||
The proxy never returns tokens over its local HTTP API. Token endpoint error
|
||||
bodies and credential-bearing URL errors are suppressed in diagnostics.
|
||||
|
||||
**Caller responsibilities:** run the proxy with the existing privileged-process
|
||||
protections. Remove `ACTIONS_ID_TOKEN_REQUEST_TOKEN` and
|
||||
`ACTIONS_ID_TOKEN_REQUEST_URL` from the environment **before** launching Codex
|
||||
or any untrusted subprocess. The proxy cannot sanitize the parent GitHub
|
||||
runner's environment for other steps. Never put GitHub or OpenAI bearer tokens
|
||||
in arguments, Codex config, server-info files, `GITHUB_OUTPUT`, or
|
||||
`GITHUB_ENV`. Retain the same privilege separation as API-key mode. The
|
||||
OIDC mode uses sensitive HTTP headers and zeroizes temporary credential
|
||||
buffers; it does not make the API-key path's single static, mlocked-allocation
|
||||
guarantee for tokens passing through the HTTP/JSON client.
|
||||
|
||||
## Behavior
|
||||
|
||||
- Reads the API key from `stdin`. All callers should pipe the key in (for example, `printenv OPENAI_API_KEY | codex-responses-api-proxy`).
|
||||
- Formats the header value as `Bearer <key>` and attempts to `mlock(2)` the memory holding that header so it is not swapped to disk.
|
||||
- By default reads the API key from `stdin`. With `--github-oidc`, reads the GitHub OIDC request credential instead; see above.
|
||||
- In API-key mode, formats the header value as `Bearer <key>` and attempts to `mlock(2)` the memory holding that header so it is not swapped to disk.
|
||||
- Listens on the provided port or an ephemeral port if `--port` is not specified.
|
||||
- Accepts exactly `POST /v1/responses` (no query string). The request body is forwarded to `https://api.openai.com/v1/responses` with `Authorization: Bearer <key>` set. All original request headers (except any incoming `Authorization`) are forwarded upstream, with `Host` overridden to `api.openai.com`. For other requests, it responds with `403`.
|
||||
- Accepts exactly `POST /v1/responses` (no query string). The request body is forwarded to the configured upstream (by default `https://api.openai.com/v1/responses`) with `Authorization` set to the selected API key or OpenAI access token. All original request headers (except any incoming `Authorization`) are forwarded upstream, with `Host` overridden to the upstream host. For other requests, it responds with `403`, aside from the optional shutdown endpoint.
|
||||
- Optionally writes a single-line JSON file with server info, currently `{ "port": <u16>, "pid": <u32> }`.
|
||||
- Optionally writes request/response JSON dumps to a directory. Each accepted request gets a pair of files that share a sequence/timestamp prefix, for example `000001-1846179912345-request.json` and `000001-1846179912345-response.json`. Header values are dumped in full except `Authorization` and any header whose name includes `cookie`, which are redacted. Bodies are written as parsed JSON when possible, otherwise as UTF-8 text.
|
||||
- Optional `--http-shutdown` enables `GET /shutdown` to terminate the process with exit code `0`. This allows one user (e.g., `root`) to start the proxy and another unprivileged user on the host to shut it down.
|
||||
@@ -70,6 +156,8 @@ curl --fail --silent --show-error "${PROXY_BASE_URL}/shutdown"
|
||||
|
||||
```
|
||||
codex-responses-api-proxy [--port <PORT>] [--server-info <FILE>] [--http-shutdown] [--upstream-url <URL>] [--dump-dir <DIR>]
|
||||
[--github-oidc --oidc-audience <AUDIENCE> --identity-provider-id <ID>
|
||||
--service-account-id <ID> --github-oidc-request-url <URL>]
|
||||
```
|
||||
|
||||
- `--port <PORT>`: Port to bind on `127.0.0.1`. If omitted, an ephemeral port is chosen.
|
||||
@@ -77,7 +165,7 @@ codex-responses-api-proxy [--port <PORT>] [--server-info <FILE>] [--http-shutdow
|
||||
- `--http-shutdown`: If set, enables `GET /shutdown` to exit the process with code `0`.
|
||||
- `--upstream-url <URL>`: Absolute URL to forward requests to. Defaults to `https://api.openai.com/v1/responses`.
|
||||
- `--dump-dir <DIR>`: If set, writes one request JSON file and one response JSON file per accepted proxy call under this directory. Filenames use a shared sequence/timestamp prefix so each pair is easy to correlate.
|
||||
- Authentication is fixed to `Authorization: Bearer <key>` to match the Codex CLI expectations.
|
||||
- Authentication is `Authorization: Bearer <key>` in API-key mode and `Authorization: Bearer <OpenAI access token>` in OIDC mode.
|
||||
|
||||
For Azure, for example (ensure your deployment accepts `Authorization: Bearer <key>`):
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// Entry point for the Codex responses API proxy binary.
|
||||
|
||||
import { spawn } from "node:child_process";
|
||||
import { closeSync } from "node:fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
@@ -61,6 +62,12 @@ const child = spawn(binaryPath, process.argv.slice(2), {
|
||||
stdio: "inherit",
|
||||
});
|
||||
|
||||
child.once("spawn", () => {
|
||||
// The child has inherited stdin. Close our copy so credential input
|
||||
// cannot be reread through the long-lived Node launcher.
|
||||
closeSync(0);
|
||||
});
|
||||
|
||||
child.on("error", (err) => {
|
||||
console.error(err);
|
||||
process.exit(1);
|
||||
|
||||
@@ -33,6 +33,8 @@ use tokio::runtime::Runtime;
|
||||
use url::Url;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
use crate::OPENAI_RESPONSES_URL;
|
||||
|
||||
const OPENAI_TOKEN_URL: &str = "https://auth.openai.com/oauth/token";
|
||||
pub(crate) const MAX_TOKEN_BYTES: usize = 16 * 1024;
|
||||
const MAX_RESPONSE_BYTES: usize = 64 * 1024;
|
||||
@@ -117,7 +119,16 @@ impl AuthHeader {
|
||||
}
|
||||
|
||||
impl Auth {
|
||||
#[cfg_attr(not(test), allow(dead_code, reason = "used by a later stack layer"))]
|
||||
/// Must be checked before reading any source credential, including stdin.
|
||||
pub(crate) fn validate_federated_upstream(upstream: &str) -> Result<()> {
|
||||
if upstream != OPENAI_RESPONSES_URL {
|
||||
return Err(anyhow!(
|
||||
"workload identity requires the default OpenAI Responses endpoint"
|
||||
));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn federated(
|
||||
source: impl SubjectTokenSource + 'static,
|
||||
provider: String,
|
||||
|
||||
@@ -166,6 +166,66 @@ fn oversized_serialized_exchange_request_is_rejected_before_sending() {
|
||||
assert!(server.try_recv().unwrap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_oidc_transport_honors_custom_ca_in_cli_blocking_context() {
|
||||
// Use a subprocess so certificate settings cannot affect parallel tests.
|
||||
// run_main also runs under spawn_blocking when invoked via the Codex CLI.
|
||||
const CHILD_ENV: &str = "CODEX_TEST_PROXY_OIDC_CUSTOM_CA";
|
||||
if std::env::var_os(CHILD_ENV).is_some() {
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
runtime.block_on(async {
|
||||
tokio::task::spawn_blocking(|| {
|
||||
let (server, url) = server();
|
||||
let auth = Auth::federated(
|
||||
RotatingSource { calls: 0 },
|
||||
"ip-test".to_owned(),
|
||||
"sa-test".to_owned(),
|
||||
)
|
||||
.unwrap();
|
||||
let Auth::Federated { state, .. } = &auth else {
|
||||
unreachable!()
|
||||
};
|
||||
// Exercise the production pool, only replacing the destination.
|
||||
state.lock().unwrap().token_url = url.join("/oauth/token").unwrap();
|
||||
let error = format!("{:#}", auth.header().unwrap_err());
|
||||
assert_eq!(error, "OpenAI workload identity token exchange failed");
|
||||
assert!(
|
||||
server
|
||||
.recv_timeout(Duration::from_millis(100))
|
||||
.unwrap()
|
||||
.is_none(),
|
||||
"must fail closed when a configured CA cannot be loaded"
|
||||
);
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
});
|
||||
return;
|
||||
}
|
||||
let output = std::process::Command::new(std::env::current_exe().unwrap())
|
||||
.args([
|
||||
"--exact",
|
||||
"auth::tests::shared_oidc_transport_honors_custom_ca_in_cli_blocking_context",
|
||||
"--nocapture",
|
||||
])
|
||||
.env(CHILD_ENV, "1")
|
||||
.env(
|
||||
"CODEX_CA_CERTIFICATE",
|
||||
"/invalid-oidc-test-ca/credential.pem",
|
||||
)
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(
|
||||
output.status.success(),
|
||||
"stdout: {}\nstderr: {}",
|
||||
String::from_utf8_lossy(&output.stdout),
|
||||
String::from_utf8_lossy(&output.stderr),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oversized_exchange_never_becomes_a_cached_credential() {
|
||||
let (server, url) = server();
|
||||
|
||||
@@ -29,7 +29,6 @@ pub(crate) struct GithubOidc {
|
||||
}
|
||||
|
||||
impl GithubOidc {
|
||||
#[expect(dead_code, reason = "CLI is wired in a later stack layer")]
|
||||
pub(crate) fn from_stdin(request_url: &str, audience: &str) -> Result<Self> {
|
||||
// Avoid echoing caller-supplied URL parameters in parsing errors.
|
||||
let mut request_url =
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use super::*;
|
||||
use crate::Args;
|
||||
use crate::auth::tests::direct_token_client;
|
||||
use clap::Parser;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use std::io::Cursor;
|
||||
@@ -11,6 +13,75 @@ use tiny_http::Server;
|
||||
use tiny_http::StatusCode;
|
||||
use tokio::runtime::Runtime;
|
||||
|
||||
const FLAGS: &[&str] = &[
|
||||
"responses-api-proxy",
|
||||
"--github-oidc",
|
||||
"--oidc-audience",
|
||||
"https://api.openai.com/v1",
|
||||
"--identity-provider-id",
|
||||
"ip-test",
|
||||
"--service-account-id",
|
||||
"sa-test",
|
||||
"--github-oidc-request-url",
|
||||
"https://example.github.test/oidc?opaque=1",
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn cli_requires_complete_oidc_configuration_and_keeps_api_key_default() {
|
||||
assert!(!Args::try_parse_from(["proxy"]).unwrap().github_oidc);
|
||||
assert!(Args::try_parse_from(["proxy", "--github-oidc"]).is_err());
|
||||
assert!(Args::try_parse_from(["proxy", "--oidc-audience", "openai"]).is_err());
|
||||
assert!(Args::try_parse_from(FLAGS).unwrap().github_oidc);
|
||||
for name in [
|
||||
"--oidc-audience",
|
||||
"--identity-provider-id",
|
||||
"--service-account-id",
|
||||
"--github-oidc-request-url",
|
||||
] {
|
||||
let pos = FLAGS.iter().position(|flag| *flag == name).unwrap();
|
||||
let mut missing = FLAGS.to_vec();
|
||||
missing.drain(pos..pos + 2);
|
||||
assert!(Args::try_parse_from(missing).is_err(), "missing {name}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oidc_disallows_provider_overrides_before_reading_any_credentials() {
|
||||
let mut flags = FLAGS.to_vec();
|
||||
flags.extend(["--upstream-url", "https://example.test/v1/responses"]);
|
||||
let args = Args::try_parse_from(flags).unwrap();
|
||||
assert!(
|
||||
crate::run_main(args)
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("default OpenAI")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unsafe_or_empty_oidc_settings_without_echoing_input() {
|
||||
for url in [
|
||||
"http://example.test?secret=sensitive",
|
||||
"https://name:sensitive@example.test/",
|
||||
"https://example.test/#sensitive",
|
||||
"sensitive",
|
||||
] {
|
||||
let mut args = Args::try_parse_from(FLAGS).unwrap();
|
||||
args.github_oidc_request_url = Some(url.to_owned());
|
||||
let Err(err) = args.auth() else {
|
||||
panic!("accepted {url}")
|
||||
};
|
||||
let error = format!("{err:#}");
|
||||
assert!(!error.contains("sensitive"), "{error}");
|
||||
}
|
||||
let mut args = Args::try_parse_from(FLAGS).unwrap();
|
||||
args.oidc_audience = Some(" ".to_owned());
|
||||
let Err(err) = args.auth() else {
|
||||
panic!("accepted empty audience")
|
||||
};
|
||||
assert!(err.to_string().contains("--oidc-audience"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn stdin_accepts_bounded_bearer_tokens_in_short_reads_and_rejects_injection() {
|
||||
let token = format!("{}.payload.sig", "a".repeat(2000));
|
||||
|
||||
@@ -30,15 +30,17 @@ use tiny_http::StatusCode;
|
||||
|
||||
mod auth;
|
||||
mod dump;
|
||||
#[cfg_attr(not(test), allow(dead_code, reason = "used by a later stack layer"))]
|
||||
mod github_oidc;
|
||||
mod read_api_key;
|
||||
use auth::Auth;
|
||||
use dump::ExchangeDumper;
|
||||
use github_oidc::GithubOidc;
|
||||
use read_api_key::read_auth_header_from_stdin;
|
||||
|
||||
const OPENAI_RESPONSES_URL: &str = "https://api.openai.com/v1/responses";
|
||||
|
||||
/// CLI arguments for the proxy.
|
||||
#[derive(Debug, Clone, Parser)]
|
||||
#[derive(Clone, Debug, Parser)]
|
||||
#[command(name = "responses-api-proxy", about = "Minimal OpenAI responses proxy")]
|
||||
pub struct Args {
|
||||
/// Port to listen on. If not set, an ephemeral port is used.
|
||||
@@ -60,6 +62,57 @@ pub struct Args {
|
||||
/// Directory where request/response dumps should be written as JSON.
|
||||
#[arg(long, value_name = "DIR")]
|
||||
pub dump_dir: Option<PathBuf>,
|
||||
|
||||
/// Authenticate to the OpenAI API using GitHub Actions workload identity.
|
||||
/// In this mode stdin supplies the GitHub OIDC request token, not an API key.
|
||||
#[arg(long, requires_all = ["oidc_audience", "identity_provider_id", "service_account_id", "github_oidc_request_url"])]
|
||||
pub github_oidc: bool,
|
||||
|
||||
/// Audience configured on the OpenAI workload identity provider.
|
||||
#[arg(long, requires = "github_oidc", value_name = "AUDIENCE")]
|
||||
pub oidc_audience: Option<String>,
|
||||
|
||||
/// OpenAI workload identity provider ID.
|
||||
#[arg(long, requires = "github_oidc", value_name = "ID")]
|
||||
pub identity_provider_id: Option<String>,
|
||||
|
||||
/// OpenAI service account ID selected by the provider mapping.
|
||||
#[arg(long, requires = "github_oidc", value_name = "ID")]
|
||||
pub service_account_id: Option<String>,
|
||||
|
||||
/// HTTPS token-request URL from GitHub's runner-provided ACTIONS_ID_TOKEN_REQUEST_URL.
|
||||
///
|
||||
/// Use the value directly from the trusted runner environment, never from
|
||||
/// a workflow input or repository content. The proxy sends the GitHub
|
||||
/// request credential to this URL.
|
||||
#[arg(long, requires = "github_oidc", value_name = "URL")]
|
||||
pub github_oidc_request_url: Option<String>,
|
||||
}
|
||||
|
||||
impl Args {
|
||||
fn auth(&self) -> Result<Auth> {
|
||||
if self.github_oidc {
|
||||
Auth::validate_federated_upstream(&self.upstream_url)?;
|
||||
let provider = required(&self.identity_provider_id, "--identity-provider-id")?;
|
||||
let service_account = required(&self.service_account_id, "--service-account-id")?;
|
||||
let audience = required(&self.oidc_audience, "--oidc-audience")?;
|
||||
let request_url = required(&self.github_oidc_request_url, "--github-oidc-request-url")?;
|
||||
Auth::federated(
|
||||
GithubOidc::from_stdin(request_url, audience)?,
|
||||
provider.to_owned(),
|
||||
service_account.to_owned(),
|
||||
)
|
||||
} else {
|
||||
Ok(Auth::ApiKey(read_auth_header_from_stdin()?))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn required<'a>(value: &'a Option<String>, name: &str) -> Result<&'a str> {
|
||||
value
|
||||
.as_deref()
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.ok_or_else(|| anyhow!("workload identity requires a nonempty {name}"))
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -75,7 +128,12 @@ struct ForwardConfig {
|
||||
|
||||
/// Entry point for the library main, for parity with other crates.
|
||||
pub fn run_main(args: Args) -> Result<()> {
|
||||
let auth = Arc::new(Auth::ApiKey(read_auth_header_from_stdin()?));
|
||||
let auth = Arc::new(args.auth()?);
|
||||
if args.github_oidc {
|
||||
// Complete the initial exchange before publishing server info.
|
||||
// API keys have already been validated when read from stdin.
|
||||
auth.header()?;
|
||||
}
|
||||
|
||||
let upstream_url = Url::parse(&args.upstream_url).context("parsing --upstream-url")?;
|
||||
let host = match (upstream_url.host_str(), upstream_url.port()) {
|
||||
|
||||
@@ -215,7 +215,7 @@ fn validate_auth_header_bytes(mut key_bytes: &[u8]) -> Result<usize> {
|
||||
}
|
||||
if key_bytes.is_empty() {
|
||||
return Err(anyhow!(
|
||||
"API key must be provided via stdin (e.g. printenv OPENAI_API_KEY | codex responses-api-proxy)"
|
||||
"API key must be provided via stdin (e.g. printenv OPENAI_API_KEY | env -u OPENAI_API_KEY codex-responses-api-proxy)"
|
||||
));
|
||||
}
|
||||
if key_bytes
|
||||
|
||||
Reference in New Issue
Block a user