Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions crates/adaptive/src/acg/request_surfaces/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ impl RequestSurface {
ProviderSurface::OCIGenAI => None,
// Gemini generateContent ACG request editing is intentionally unsupported.
ProviderSurface::GeminiGenerateContent => None,
// ACG prompt mutation is not defined for evaluation state/questions.
ProviderSurface::TypeSafeSystemOne => None,
}
}

Expand Down
3 changes: 3 additions & 0 deletions crates/adaptive/src/response_cache/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,9 @@ fn lossy_request_shape(surface: ProviderSurface, content: &Json) -> bool {
.and_then(Json::as_array)
.is_some_and(|items| items.iter().any(lossy_gemini_content_item))
}
// The codec losslessly preserves evaluation state, questions, and
// provider extensions, so normalized cache keys are safe.
ProviderSurface::TypeSafeSystemOne => false,
}
}

Expand Down
2 changes: 2 additions & 0 deletions crates/adaptive/src/response_cache/replay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ fn synthesize_replay_chunks(aggregate: &Json) -> Option<Vec<Json>> {
// re-aggregates it and rejects shapes the streaming collector cannot
// preserve exactly.
ProviderSurface::GeminiGenerateContent => vec![aggregate.clone()],
// System One is explicitly non-streaming, so no replay chunks exist.
ProviderSurface::TypeSafeSystemOne => return None,
})
}

Expand Down
1 change: 1 addition & 0 deletions crates/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ futures-util = "0.3"
fs2 = "0.4"
http = "1"
http-body-util = "0.1"
httpdate = "1"
hyper = { version = "1.11.1", features = ["client", "server", "http1", "http2"] }
hyper-rustls = { version = "0.27", default-features = false, features = ["http1", "http2", "native-tokio", "ring", "tls12"] }
hyper-util = { version = "0.1", features = ["client-legacy", "http1", "http2", "server-auto", "service", "tokio"] }
Expand Down
10 changes: 9 additions & 1 deletion crates/cli/src/agents/shared/alignment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,16 +51,20 @@ pub(crate) enum GatewayRouteKind {
OpenAiModels,
AnthropicMessages,
AnthropicCountTokens,
TypeSafeSystemOne,
TypeSafeModels,
}

impl GatewayRouteKind {
pub(crate) const ALL: [Self; 6] = [
pub(crate) const ALL: [Self; 8] = [
Self::OpenAiResponses,
Self::OpenAiChatCompletions,
Self::OpenAiImagesGenerations,
Self::OpenAiModels,
Self::AnthropicMessages,
Self::AnthropicCountTokens,
Self::TypeSafeSystemOne,
Self::TypeSafeModels,
];

pub(crate) const fn name(self) -> &'static str {
Expand All @@ -71,6 +75,8 @@ impl GatewayRouteKind {
Self::OpenAiModels => "openai.models",
Self::AnthropicMessages => "anthropic.messages",
Self::AnthropicCountTokens => "anthropic.count_tokens",
Self::TypeSafeSystemOne => "typesafe.system_one",
Self::TypeSafeModels => "typesafe.models",
}
}

Expand Down Expand Up @@ -495,6 +501,8 @@ fn provider_request_extractor(route: GatewayRouteKind) -> &'static dyn ProviderR
GatewayRouteKind::OpenAiModels => &OPENAI_MODELS_REQUEST_EXTRACTOR,
GatewayRouteKind::AnthropicMessages => &ANTHROPIC_MESSAGES_REQUEST_EXTRACTOR,
GatewayRouteKind::AnthropicCountTokens => &ANTHROPIC_COUNT_TOKENS_REQUEST_EXTRACTOR,
GatewayRouteKind::TypeSafeSystemOne => &OPENAI_MODELS_REQUEST_EXTRACTOR,
GatewayRouteKind::TypeSafeModels => &OPENAI_MODELS_REQUEST_EXTRACTOR,
}
}

Expand Down
16 changes: 12 additions & 4 deletions crates/cli/src/commands/configure/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,15 +105,23 @@ impl ConfigDocument {
}

fn has_auth_headers(&self) -> bool {
["openai_auth_header", "anthropic_auth_header"]
.into_iter()
.any(|key| self.has_key("upstream", key))
[
"openai_auth_header",
"anthropic_auth_header",
"typesafe_auth_header",
]
.into_iter()
.any(|key| self.has_key("upstream", key))
}

fn preview(&self) -> String {
let mut document = self.document.clone();
if let Some(upstream) = document.get_mut("upstream") {
for key in ["openai_auth_header", "anthropic_auth_header"] {
for key in [
"openai_auth_header",
"anthropic_auth_header",
"typesafe_auth_header",
] {
if let Some(table) = upstream.as_table_mut() {
if table.contains_key(key) {
table[key] = value("<redacted>");
Expand Down
12 changes: 11 additions & 1 deletion crates/cli/src/commands/configure/editor/prompt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,14 +126,24 @@ fn edit_upstream(theme: &ColorfulTheme, document: &mut ConfigDocument) -> Result
"Anthropic authorization header: {}",
document.secret_summary("anthropic_auth_header")
),
format!(
"TypeSafe base URL: {}",
document.string_summary("upstream", "typesafe_base_url")
),
format!(
"TypeSafe authorization header: {}",
document.secret_summary("typesafe_auth_header")
),
"Back".into(),
];
match select(theme, "Provider upstreams", &choices)? {
0 => edit_string(theme, document, "upstream", "openai_base_url")?,
1 => edit_secret(theme, document, "openai_auth_header")?,
2 => edit_string(theme, document, "upstream", "anthropic_base_url")?,
3 => edit_secret(theme, document, "anthropic_auth_header")?,
4 => return Ok(()),
4 => edit_string(theme, document, "upstream", "typesafe_base_url")?,
5 => edit_secret(theme, document, "typesafe_auth_header")?,
6 => return Ok(()),
_ => unreachable!(),
}
}
Expand Down
4 changes: 4 additions & 0 deletions crates/cli/src/commands/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ pub(crate) struct RunCommand {
#[arg(long)]
pub(super) anthropic_base_url: Option<String>,
#[arg(long)]
pub(super) typesafe_base_url: Option<String>,
#[arg(long)]
pub(super) session_metadata: Option<String>,
#[arg(long, env = "NEMO_RELAY_PLUGIN_CONFIG_PATH", hide = true)]
pub(super) plugin_config_path: Option<PathBuf>,
Expand All @@ -50,6 +52,7 @@ impl RunCommand {
config: self.config,
openai_base_url: self.openai_base_url,
anthropic_base_url: self.anthropic_base_url,
typesafe_base_url: self.typesafe_base_url,
session_metadata: self.session_metadata,
plugin_config_path: self.plugin_config_path,
dry_run: self.dry_run,
Expand Down Expand Up @@ -106,6 +109,7 @@ pub(super) async fn easy_path(
config: explicit_config.map(PathBuf::from),
openai_base_url: None,
anthropic_base_url: None,
typesafe_base_url: None,
session_metadata: None,
plugin_config_path: None,
dry_run: command.dry_run,
Expand Down
4 changes: 4 additions & 0 deletions crates/cli/src/commands/serve.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,9 @@ pub(crate) struct ServerArgs {
/// Upstream Anthropic base URL (e.g. https://api.anthropic.com)
#[arg(long, env = "NEMO_RELAY_ANTHROPIC_BASE_URL")]
pub(super) anthropic_base_url: Option<String>,
/// Upstream TypeSafe base URL (e.g. https://api.typesafe.ai/v1)
#[arg(long, env = "NEMO_RELAY_TYPESAFE_BASE_URL")]
pub(super) typesafe_base_url: Option<String>,
/// Internal override for the plugin configuration file.
#[arg(long, env = "NEMO_RELAY_PLUGIN_CONFIG_PATH", hide = true)]
pub(super) plugin_config_path: Option<PathBuf>,
Expand All @@ -41,6 +44,7 @@ impl ServerArgs {
bind: self.bind,
openai_base_url: self.openai_base_url.clone(),
anthropic_base_url: self.anthropic_base_url.clone(),
typesafe_base_url: self.typesafe_base_url.clone(),
plugin_config_path: self.plugin_config_path.clone(),
ready_file: self.ready_file.clone(),
max_hook_payload_bytes: self.max_hook_payload_bytes,
Expand Down
87 changes: 87 additions & 0 deletions crates/cli/src/configuration/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,10 @@ struct FileUpstreamConfig {
openai_auth_header: Option<String>,
anthropic_base_url: Option<String>,
anthropic_auth_header: Option<String>,
typesafe_base_url: Option<String>,
typesafe_auth_header: Option<String>,
typesafe_max_retries: Option<u32>,
typesafe_max_retry_delay_ms: Option<u64>,
}

#[derive(Debug, Clone, Default, Deserialize)]
Expand Down Expand Up @@ -297,6 +301,10 @@ fn persistent_bootstrap_fingerprint(
"openai_auth_header": gateway.openai_auth_header,
"anthropic_base_url": gateway.anthropic_base_url,
"anthropic_auth_header": gateway.anthropic_auth_header,
"typesafe_base_url": gateway.typesafe_base_url,
"typesafe_auth_header": gateway.typesafe_auth_header,
"typesafe_max_retries": gateway.typesafe_retry.max_retries,
"typesafe_max_retry_delay_ms": gateway.typesafe_retry.max_server_delay.as_millis(),
"metadata": gateway.metadata,
"plugin_config": gateway.plugin_config,
"max_hook_payload_bytes": gateway.max_hook_payload_bytes,
Expand Down Expand Up @@ -1101,6 +1109,13 @@ fn apply_run_url_overrides(config: &mut GatewayConfig, command: &RunOverrides) {
value.clone(),
);
}
if let Some(value) = &command.typesafe_base_url {
replace_upstream_base_url(
&mut config.typesafe_base_url,
&mut config.typesafe_auth_header,
value.clone(),
);
}
}

// Parses JSON-bearing run overrides after simple values. Invalid metadata or plugin config fails
Expand Down Expand Up @@ -1138,6 +1153,13 @@ fn apply_server_overrides(
value.clone(),
);
}
if let Some(value) = &args.typesafe_base_url {
replace_upstream_base_url(
&mut config.typesafe_base_url,
&mut config.typesafe_auth_header,
value.clone(),
);
}
if let Some(value) = args.max_hook_payload_bytes {
config.max_hook_payload_bytes = validate_body_limit("max hook payload bytes", value)?;
}
Expand Down Expand Up @@ -1397,6 +1419,10 @@ fn apply_file_upstream_config(
openai_auth_header,
anthropic_base_url,
anthropic_auth_header,
typesafe_base_url,
typesafe_auth_header,
typesafe_max_retries,
typesafe_max_retry_delay_ms,
} = upstream;
if let Some(value) = openai_base_url {
gateway.openai_base_url = value;
Expand All @@ -1420,6 +1446,24 @@ fn apply_file_upstream_config(
value,
)?);
}
if let Some(value) = typesafe_base_url {
gateway.typesafe_base_url = value;
if typesafe_auth_header.is_none() {
gateway.typesafe_auth_header = None;
}
}
if let Some(value) = typesafe_auth_header {
gateway.typesafe_auth_header = Some(validate_auth_header(
"upstream.typesafe_auth_header",
value,
)?);
}
if let Some(value) = typesafe_max_retries {
gateway.typesafe_retry.max_retries = value;
}
if let Some(value) = typesafe_max_retry_delay_ms {
gateway.typesafe_retry.max_server_delay = Duration::from_millis(value);
}
Ok(())
}

Expand Down Expand Up @@ -1724,6 +1768,30 @@ fn apply_env_config(config: &mut GatewayConfig) -> Result<(), CliError> {
value,
)?);
}
let typesafe_auth_header = std::env::var("NEMO_RELAY_TYPESAFE_AUTH_HEADER").ok();
if let Ok(value) = std::env::var("NEMO_RELAY_TYPESAFE_BASE_URL") {
replace_upstream_base_url(
&mut config.typesafe_base_url,
&mut config.typesafe_auth_header,
value,
);
}
if let Some(value) = typesafe_auth_header {
config.typesafe_auth_header = Some(validate_auth_header(
"NEMO_RELAY_TYPESAFE_AUTH_HEADER",
value,
)?);
}
if let Ok(value) = std::env::var("NEMO_RELAY_TYPESAFE_MAX_RETRIES") {
config.typesafe_retry.max_retries =
parse_retry_u32("NEMO_RELAY_TYPESAFE_MAX_RETRIES", &value)?;
}
if let Ok(value) = std::env::var("NEMO_RELAY_TYPESAFE_MAX_RETRY_DELAY_MS") {
config.typesafe_retry.max_server_delay = Duration::from_millis(parse_retry_u64(
"NEMO_RELAY_TYPESAFE_MAX_RETRY_DELAY_MS",
&value,
)?);
}
if let Ok(value) = std::env::var("NEMO_RELAY_MAX_HOOK_PAYLOAD_BYTES") {
config.max_hook_payload_bytes =
parse_env_body_limit("NEMO_RELAY_MAX_HOOK_PAYLOAD_BYTES", &value)?;
Expand Down Expand Up @@ -1751,6 +1819,12 @@ fn apply_managed_worker_env_config(config: &mut GatewayConfig) -> Result<(), Cli
value,
)?);
}
if let Ok(value) = std::env::var("NEMO_RELAY_TYPESAFE_AUTH_HEADER") {
config.typesafe_auth_header = Some(validate_auth_header(
"NEMO_RELAY_TYPESAFE_AUTH_HEADER",
value,
)?);
}
Ok(())
}

Expand Down Expand Up @@ -1782,6 +1856,18 @@ fn parse_env_body_limit(name: &str, raw: &str) -> Result<usize, CliError> {
validate_body_limit(name, value)
}

fn parse_retry_u32(name: &str, raw: &str) -> Result<u32, CliError> {
raw.trim()
.parse::<u32>()
.map_err(|_| CliError::Config(format!("{name} must be a non-negative integer")))
}

fn parse_retry_u64(name: &str, raw: &str) -> Result<u64, CliError> {
raw.trim()
.parse::<u64>()
.map_err(|_| CliError::Config(format!("{name} must be a non-negative integer")))
}

fn validate_body_limit(name: &str, value: usize) -> Result<usize, CliError> {
if value == 0 {
return Err(CliError::Config(format!("{name} must be greater than 0")));
Expand Down Expand Up @@ -1824,6 +1910,7 @@ fn clear_credentials_for_replaced_upstreams(left: &mut toml::Value, right: &toml
for (base_url, auth_header) in [
("openai_base_url", "openai_auth_header"),
("anthropic_base_url", "anthropic_auth_header"),
("typesafe_base_url", "typesafe_auth_header"),
] {
let endpoint_changed = override_upstream
.get(base_url)
Expand Down
6 changes: 6 additions & 0 deletions crates/cli/src/configuration/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ pub(crate) struct GatewayConfig {
pub(crate) openai_auth_header: Option<String>,
pub(crate) anthropic_base_url: String,
pub(crate) anthropic_auth_header: Option<String>,
pub(crate) typesafe_base_url: String,
pub(crate) typesafe_auth_header: Option<String>,
pub(crate) typesafe_retry: crate::typesafe_retry::TypeSafeRetryPolicy,
pub(crate) metadata: Option<Value>,
pub(crate) plugin_config: Option<Value>,
pub(crate) max_hook_payload_bytes: usize,
Expand Down Expand Up @@ -115,6 +118,9 @@ impl Default for GatewayConfig {
openai_auth_header: None,
anthropic_base_url: "https://api.anthropic.com".into(),
anthropic_auth_header: None,
typesafe_base_url: "https://api.typesafe.ai/v1".into(),
typesafe_auth_header: None,
typesafe_retry: crate::typesafe_retry::TypeSafeRetryPolicy::default(),
metadata: None,
plugin_config: None,
max_hook_payload_bytes: DEFAULT_MAX_HOOK_PAYLOAD_BYTES,
Expand Down
Loading