feat: drive Responses behavior from a dialect profile (#7261)

* feat(config): load Responses dialect profiles from a checked JSON table

Responses behavior is still chosen by ProviderKind. The table gives routing a validated profile id before the engine starts consuming it.

* feat(config): stamp a Responses profile id onto the resolved route

The dialect table existed without a route to carry it. Selection now happens after the protocol is known, so Chat routes stay empty and Responses routes cannot be minted without a profile.

* feat(config): let a custom provider name its Responses profile

A custom table could set wire to responses but not name the dialect, so every such route inherited standard. The field keeps that default, and rejects a built-in table, a non-Responses wire, or any profile outside the custom allowlist.

* feat(engine): carry the resolved Responses profile into the client

Fixed-wire clients were built from parts and dropped the profile the route had already chosen. The id now stays on the client and the prepared request; a catalog probe does not invent one, and a Responses client without an id fails before the body is built.

* feat(engine): build Responses requests from the admitted profile

The client already carried a profile id, but the body and replay still branched on the provider. Those choices now come from the dialect profile, while encrypted scope stays with the caller.

* feat(engine): drive Responses URLs and stream termination from the profile

The request body already followed the dialect profile, but the endpoint and SSE completion still branched on the provider. Path policy now chooses the URL, including DeepSeek search, and strict completion stays on the profile while account-sharing errors stay on the Codex identity.

* fix(engine): keep the standard profile helper out of the library build

The helper is only called from tests, and the library build denies unused code.

* feat(tui): record Responses route, request, and stream fixtures

Profile selection, the prepared body, and termination now have a compare-only family. It uses the existing resolver, prepare path, and loopback instead of a second parser.

* fix(tui): give the Codex request fixture its own test grant

The official route will not build a client from a process token. Isolate CODEWHALE_HOME and install the existing test registration so the request golden is compared.

conformance::responses and conformance::sse: 6 passed; 0 failed.

* fix(config): check the dialect table only in tests; restore the wire dialect doc

Review follow-up for this PR. The embedded dialect JSON was re-parsed
and re-generated at runtime by ensure_asset() on every lookup, which put
the parser in the library build although build.rs already validates the
same file at compile time. The parser module is now test-only and the
check is the embedded_asset_matches_the_generated_table test.
provider_responses_profile was inserted between provider_wire_dialect
and its doc comment; the doc is back on its function.

Co-Authored-By: Claude Opus 5.5 <[email protected]>

---------

Co-authored-by: CodeWhale Bot <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-11-10 21:04:55 +08:00
1 parent 64fb68b3f0
commit b3fa8d6d3f
79 files changed
+3471 -171

No files matched your search

@@ -0,0 +1,210 @@
{
"schema_version": 1,
"default_profile": "standard",
"custom_allowed_profiles": ["standard"],
"profiles": [
{
"id": "standard",
"endpoint": { "path": "versioned_responses" },
"headers": { "protocol_headers": "standard_json" },
"request": {
"store": "explicit_false",
"include": "encrypted_reasoning",
"instructions": "top_level",
"max_output_tokens": "when_positive",
"sampling": "omit"
},
"reasoning": {
"effort_mapping": "openai_compatible",
"shape": "effort_with_summary",
"summary": "auto"
},
"replay": {
"encrypted_state": "provider_api_model_scoped",
"plain_reasoning": "ignore",
"system_role": "preserve"
},
"tools": {
"envelope": "function_array",
"namespace": null,
"parallel_calls": "allow"
},
"stream": {
"malformed_json": "skip_with_diagnostic",
"unknown_event": "ignore",
"done_marker": "terminal",
"completion": "accept_documented_terminal_events",
"incomplete": "preserve_provider_incomplete_result"
},
"usage": {
"cache_fields": "top_level_then_nested_fallback",
"reasoning_fields": "output_tokens_details",
"audio_fields": "nested_details"
},
"capabilities": {
"function_tools": "supported",
"server_side_tools": "route_and_adapter_gated"
}
},
{
"id": "codex",
"endpoint": { "path": "versioned_responses" },
"headers": { "protocol_headers": "standard_json" },
"request": {
"store": "explicit_false",
"include": "encrypted_reasoning",
"instructions": "top_level",
"max_output_tokens": "omit",
"sampling": "omit"
},
"reasoning": {
"effort_mapping": "codex",
"shape": "effort_with_summary",
"summary": "auto"
},
"replay": {
"encrypted_state": "account_api_model_scoped",
"plain_reasoning": "ignore",
"system_role": "system_to_developer"
},
"tools": {
"envelope": "namespace_wrapped",
"namespace": "codewhale",
"parallel_calls": "disallow"
},
"stream": {
"malformed_json": "fail",
"unknown_event": "ignore",
"done_marker": "ignore",
"completion": "require_successful_response_completed",
"incomplete": "fail"
},
"usage": {
"cache_fields": "top_level_then_nested_fallback",
"reasoning_fields": "output_tokens_details",
"audio_fields": "nested_details"
},
"capabilities": {
"function_tools": "supported",
"server_side_tools": "route_and_adapter_gated"
}
},
{
"id": "deepseek_stateless_plain_reasoning",
"endpoint": { "path": "unversioned_responses" },
"headers": { "protocol_headers": "standard_json" },
"request": {
"store": "omit",
"include": "omit",
"instructions": "top_level",
"max_output_tokens": "when_positive",
"sampling": "temperature_and_top_p"
},
"reasoning": {
"effort_mapping": "deepseek",
"shape": "effort_only",
"summary": "omit"
},
"replay": {
"encrypted_state": "provider_api_model_scoped",
"plain_reasoning": "plain_text_with_tool_placeholder",
"system_role": "preserve"
},
"tools": {
"envelope": "function_array",
"namespace": null,
"parallel_calls": "allow"
},
"stream": {
"malformed_json": "skip_with_diagnostic",
"unknown_event": "ignore",
"done_marker": "terminal",
"completion": "accept_documented_terminal_events",
"incomplete": "preserve_provider_incomplete_result"
},
"usage": {
"cache_fields": "top_level_then_nested_fallback",
"reasoning_fields": "output_tokens_details",
"audio_fields": "nested_details"
},
"capabilities": {
"function_tools": "supported",
"server_side_tools": "route_and_adapter_gated"
}
},
{
"id": "concentrate",
"endpoint": { "path": "versioned_responses" },
"headers": { "protocol_headers": "standard_json" },
"request": {
"store": "omit",
"include": "omit",
"instructions": "leading_system_input",
"max_output_tokens": "when_positive",
"sampling": "omit"
},
"reasoning": {
"effort_mapping": "openai_compatible",
"shape": "effort_only",
"summary": "omit"
},
"replay": {
"encrypted_state": "provider_api_model_scoped",
"plain_reasoning": "ignore",
"system_role": "preserve"
},
"tools": {
"envelope": "function_array",
"namespace": null,
"parallel_calls": "allow"
},
"stream": {
"malformed_json": "skip_with_diagnostic",
"unknown_event": "ignore",
"done_marker": "terminal",
"completion": "accept_documented_terminal_events",
"incomplete": "preserve_provider_incomplete_result"
},
"usage": {
"cache_fields": "top_level_then_nested_fallback",
"reasoning_fields": "output_tokens_details",
"audio_fields": "nested_details"
},
"capabilities": {
"function_tools": "supported",
"server_side_tools": "route_and_adapter_gated"
}
}
],
"bindings": [
{
"provider_kind": "OpenaiCodex",
"protocol": "responses",
"base_url_match": { "type": "any" },
"profile": "codex"
},
{
"provider_kind": "Concentrate",
"protocol": "responses",
"base_url_match": { "type": "any" },
"profile": "concentrate"
},
{
"provider_kind": "Deepseek",
"protocol": "responses",
"endpoint_key": "responses",
"base_url_match": {
"type": "exact_normalized",
"urls": [
"https://api.deepseek.com",
"https://api.deepseek.com/v1",
"https://api.deepseek.com/beta",
"https://api.deepseeki.com",
"https://api.deepseeki.com/v1",
"https://api.deepseeki.com/beta"
]
},
"profile": "deepseek_stateless_plain_reasoning"
}
]
}
+18
View File
@@ -8,6 +8,9 @@ use std::{
use serde_json::Value;
#[path = "src/responses_dialect_data.rs"]
mod responses_dialect_data;
fn text<'a>(row: &'a Value, key: &str) -> &'a str {
row[key]
.as_str()
@@ -585,4 +588,19 @@ fn main() {
catalog_constants.push_str("_ => &[], } }\n");
fs::write(directory.join("catalog_constants.rs"), catalog_constants)
.expect("write catalog constants");
write_responses_dialects();
}
fn write_responses_dialects() {
println!("cargo:rerun-if-changed=assets/responses_dialects.json");
println!("cargo:rerun-if-changed=src/responses_dialect_data.rs");
let raw = fs::read_to_string("assets/responses_dialects.json").expect("responses dialects");
let doc = responses_dialect_data::parse(&raw)
.unwrap_or_else(|err| panic!("responses dialects: {err}"));
let directory = PathBuf::from(env::var_os("OUT_DIR").expect("OUT_DIR"));
fs::write(
directory.join("responses_dialects.rs"),
responses_dialect_data::codegen(&doc),
)
.expect("write responses dialects");
}
+1
View File
@@ -1009,6 +1009,7 @@ fn offline_resolver_keeps_images_for_a_text_only_seed_row() {
base_url_override: None,
limit_overrides: Vec::new(),
wire_override: None,
responses_profile_override: None,
})
.expect("bundled Anthropic route resolves offline");
assert_ne!(
+1
View File
@@ -1082,6 +1082,7 @@ impl ConfigToml {
base_url_override: Some(base_url.clone()),
limit_overrides: Vec::new(),
wire_override: None,
responses_profile_override: None,
});
let mut http_headers = self.http_headers.clone();
+1
View File
@@ -25,6 +25,7 @@ mod provider_defaults;
mod provider_kind;
pub mod redaction;
pub mod resolve;
pub mod responses_dialect;
pub mod route;
pub mod settings_schema;
pub mod setup_state;
+89
View File
@@ -525,11 +525,100 @@ pub fn wire_dialect_override(table: &str, wire: Option<&str>) -> Option<WireForm
}
}
/// The explicit Responses profile a custom table may request.
///
/// An empty value selects nothing, so an existing `wire = "responses"` table
/// keeps the default profile. A set value is rejected unless this is a custom
/// provider whose wire dialect is Responses and the id is on the custom
/// allowlist. Built-in profiles stay on their route bindings.
pub fn responses_profile_override(
kind: crate::ProviderKind,
table: &str,
wire: Option<&str>,
profile: Option<&str>,
) -> Result<Option<String>, String> {
let Some(profile) = profile.map(str::trim).filter(|value| !value.is_empty()) else {
return Ok(None);
};
if kind != crate::ProviderKind::Custom {
return Err(format!("provider {table} cannot set responses_profile"));
}
if !wire_dialect_prefers_responses(wire) {
return Err(format!(
"provider {table} responses_profile `{profile}` requires wire = \"responses\""
));
}
crate::responses_dialect::accept_custom_responses_profile(profile)
.map_err(|err| format!("provider {table}: {err}"))?;
Ok(Some(profile.to_string()))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::descriptors::defaults::*;
#[test]
fn responses_profile_override_rejects_the_wrong_wire_and_identity() {
assert_eq!(
responses_profile_override(
crate::ProviderKind::Custom,
"gateway",
Some("responses"),
None
)
.expect("absent"),
None
);
assert_eq!(
responses_profile_override(
crate::ProviderKind::Custom,
"gateway",
Some("responses"),
Some(" standard "),
)
.expect("standard"),
Some("standard".to_string())
);
for (kind, wire, profile, needle) in [
(
crate::ProviderKind::Openai,
Some("responses"),
Some("standard"),
"cannot set",
),
(
crate::ProviderKind::Custom,
Some("chat"),
Some("standard"),
"requires wire",
),
(
crate::ProviderKind::Custom,
Some("anthropic"),
Some("standard"),
"requires wire",
),
(
crate::ProviderKind::Custom,
Some("responses"),
Some("codex"),
"allowed: standard",
),
(
crate::ProviderKind::Custom,
Some("responses"),
Some("missing"),
"unknown responses profile",
),
] {
let error = responses_profile_override(kind, "gateway", wire, profile)
.expect_err("rejected profile");
assert!(error.contains(needle), "{error}");
assert!(error.contains("gateway"), "{error}");
}
}
#[test]
fn wire_dialect_override_parses_the_canonical_alias_sets() {
// The exact alias sets are the cross-crate contract: the tui route
+6
View File
@@ -130,6 +130,11 @@ pub struct ProviderConfigToml {
alias = "dialect"
)]
pub wire: Option<String>,
/// Responses dialect for a custom table whose `wire` is `responses`.
/// Absent keeps the default `standard` profile. Built-in providers reject
/// this field; `codex` is not on the custom allowlist.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub responses_profile: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auth_mode: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
@@ -175,6 +180,7 @@ impl ProviderConfigToml {
&& self.model_context_windows.is_empty()
&& blank(self.mode.as_ref())
&& blank(self.wire.as_ref())
&& blank(self.responses_profile.as_ref())
&& blank(self.auth_mode.as_ref())
&& self.insecure_skip_tls_verify.is_none()
&& self.allow_insecure_http.is_none()
+755
View File
@@ -0,0 +1,755 @@
//! Responses dialect profiles loaded from `assets/responses_dialects.json`.
//!
//! Known limits: selection does not mint a route or build a request. A
//! DeepSeek Responses URL outside the binding's exact list does not take the
//! DeepSeek profile. Custom DeepSeek endpoints are forced to Chat by the route
//! resolver before selection. Replay API scope strings and account identity
//! stay with the caller. All four profiles share one usage policy.
use serde::Serialize;
use crate::ProviderKind;
use crate::provider::WireFormat;
#[cfg(test)]
#[path = "responses_dialect_data.rs"]
mod data;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize)]
#[serde(transparent)]
pub struct ResponsesProfileId(&'static str);
impl ResponsesProfileId {
const fn from_static(id: &'static str) -> Self {
Self(id)
}
#[must_use]
pub const fn as_str(self) -> &'static str {
self.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResponsesDialectProfile {
pub id: ResponsesProfileId,
pub endpoint: ResponsesEndpointPolicy,
pub headers: ResponsesHeaderPolicy,
pub request: ResponsesRequestPolicy,
pub reasoning: ResponsesReasoningPolicy,
pub replay: ResponsesReplayPolicy,
pub tools: ResponsesToolPolicy,
pub stream: ResponsesStreamPolicy,
pub usage: ResponsesUsagePolicy,
pub capabilities: ResponsesCapabilityPolicy,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResponsesEndpointPolicy {
pub path: ResponsesPathPolicy,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ResponsesPathPolicy {
VersionedResponses,
UnversionedResponses,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResponsesHeaderPolicy {
pub protocol_headers: ProtocolHeaderPolicy,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ProtocolHeaderPolicy {
StandardJson,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResponsesRequestPolicy {
pub store: StorePolicy,
pub include: IncludePolicy,
pub instructions: InstructionsPlacement,
pub max_output_tokens: OutputTokenFieldPolicy,
pub sampling: SamplingPolicy,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StorePolicy {
ExplicitFalse,
Omit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IncludePolicy {
EncryptedReasoning,
Omit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum InstructionsPlacement {
TopLevel,
LeadingSystemInput,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum OutputTokenFieldPolicy {
WhenPositive,
Omit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SamplingPolicy {
TemperatureAndTopP,
Omit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResponsesReasoningPolicy {
pub effort_mapping: ReasoningEffortMapping,
pub shape: ReasoningRequestShape,
pub summary: ReasoningSummaryPolicy,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReasoningEffortMapping {
OpenaiCompatible,
Codex,
Deepseek,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReasoningRequestShape {
EffortWithSummary,
EffortOnly,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReasoningSummaryPolicy {
Auto,
Omit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResponsesReplayPolicy {
pub encrypted_state: EncryptedReasoningReplayPolicy,
pub plain_reasoning: PlainReasoningReplayPolicy,
pub system_role: SystemRoleReplayPolicy,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EncryptedReasoningReplayPolicy {
ProviderApiModelScoped,
AccountApiModelScoped,
Disabled,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PlainReasoningReplayPolicy {
Ignore,
PlainText,
PlainTextWithToolPlaceholder,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SystemRoleReplayPolicy {
Preserve,
SystemToDeveloper,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResponsesToolPolicy {
pub envelope: ToolEnvelopePolicy,
pub namespace: Option<&'static str>,
pub parallel_calls: ParallelToolCallPolicy,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ToolEnvelopePolicy {
FunctionArray,
NamespaceWrapped,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ParallelToolCallPolicy {
Allow,
Disallow,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResponsesStreamPolicy {
pub malformed_json: MalformedEventPolicy,
pub unknown_event: UnknownEventPolicy,
pub done_marker: DoneMarkerPolicy,
pub completion: CompletionPolicy,
pub incomplete: IncompleteResponsePolicy,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum MalformedEventPolicy {
Fail,
SkipWithDiagnostic,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum UnknownEventPolicy {
Ignore,
Fail,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DoneMarkerPolicy {
Terminal,
Ignore,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CompletionPolicy {
AcceptDocumentedTerminalEvents,
RequireSuccessfulResponseCompleted,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IncompleteResponsePolicy {
PreserveProviderIncompleteResult,
Fail,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResponsesUsagePolicy {
pub cache_fields: CacheUsageFieldPolicy,
pub reasoning_fields: ReasoningUsageFieldPolicy,
pub audio_fields: AudioUsageFieldPolicy,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CacheUsageFieldPolicy {
TopLevelThenNestedFallback,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ReasoningUsageFieldPolicy {
OutputTokensDetails,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AudioUsageFieldPolicy {
NestedDetails,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResponsesCapabilityPolicy {
pub function_tools: FunctionToolCapability,
pub server_side_tools: ServerSideToolCapability,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FunctionToolCapability {
Supported,
Unsupported,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ServerSideToolCapability {
RouteAndAdapterGated,
Unsupported,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResponsesProfileBinding {
pub provider_kind: ProviderKind,
pub endpoint_key: Option<&'static str>,
pub base_urls: Option<&'static [&'static str]>,
pub profile_id: ResponsesProfileId,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ResponsesProfileQuery<'a> {
pub protocol: WireFormat,
pub provider: ProviderKind,
pub endpoint_key: &'a str,
pub base_url: &'a str,
pub explicit_profile: Option<&'a str>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResponsesProfileError {
NotResponses { profile: String },
UnknownProfile { profile: String, allowed: String },
ProfileNotAllowed { profile: String, allowed: String },
BuiltinOverride { provider: ProviderKind },
AmbiguousBinding { provider: ProviderKind },
}
impl std::fmt::Display for ResponsesProfileError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NotResponses { profile } => write!(
formatter,
"responses profile `{profile}` cannot be set when the wire is not responses"
),
Self::UnknownProfile { profile, allowed } => write!(
formatter,
"unknown responses profile `{profile}`; allowed: {allowed}"
),
Self::ProfileNotAllowed { profile, allowed } => write!(
formatter,
"responses profile `{profile}` is not allowed for a custom provider; allowed: {allowed}"
),
Self::BuiltinOverride { provider } => write!(
formatter,
"built-in provider {} cannot set a responses profile",
provider.as_str()
),
Self::AmbiguousBinding { provider } => write!(
formatter,
"responses profile bindings overlap for {}",
provider.as_str()
),
}
}
}
impl std::error::Error for ResponsesProfileError {}
include!(concat!(env!("OUT_DIR"), "/responses_dialects.rs"));
#[must_use]
pub fn builtin_profiles() -> &'static [ResponsesDialectProfile] {
BUILTIN_PROFILES
}
pub fn profile(id: &str) -> Option<&'static ResponsesDialectProfile> {
find_profile(id)
}
/// Path policy for a base URL that matches an exact binding. Unmatched URLs
/// stay on the versioned Responses path. Matching strips a trailing slash and
/// lowercases; it does not trim surrounding whitespace.
pub fn bound_responses_path(provider: ProviderKind, base_url: &str) -> ResponsesPathPolicy {
let normalized = base_url.trim_end_matches('/').to_ascii_lowercase();
for binding in BUILTIN_BINDINGS {
if binding.provider_kind != provider {
continue;
}
let Some(urls) = binding.base_urls else {
continue;
};
if !urls.iter().any(|url| *url == normalized) {
continue;
}
if let Some(profile) = find_profile(binding.profile_id.as_str()) {
return profile.endpoint.path;
}
}
ResponsesPathPolicy::VersionedResponses
}
pub fn accept_custom_responses_profile(
profile: &str,
) -> Result<&'static ResponsesDialectProfile, ResponsesProfileError> {
let Some(found) = find_profile(profile) else {
return Err(ResponsesProfileError::UnknownProfile {
profile: profile.to_string(),
allowed: allowed_list(),
});
};
if !CUSTOM_ALLOWED_PROFILES.contains(&found.id) {
return Err(ResponsesProfileError::ProfileNotAllowed {
profile: profile.to_string(),
allowed: allowed_list(),
});
}
Ok(found)
}
pub fn select_responses_profile(
query: ResponsesProfileQuery<'_>,
) -> Result<Option<&'static ResponsesDialectProfile>, ResponsesProfileError> {
if query.protocol != WireFormat::Responses {
return match query.explicit_profile {
Some(profile) => Err(ResponsesProfileError::NotResponses {
profile: profile.to_string(),
}),
None => Ok(None),
};
}
if let Some(explicit) = query.explicit_profile {
if query.provider != ProviderKind::Custom {
return Err(ResponsesProfileError::BuiltinOverride {
provider: query.provider,
});
}
let Some(profile) = find_profile(explicit) else {
return Err(ResponsesProfileError::UnknownProfile {
profile: explicit.to_string(),
allowed: allowed_list(),
});
};
if !CUSTOM_ALLOWED_PROFILES.contains(&profile.id) {
return Err(ResponsesProfileError::ProfileNotAllowed {
profile: explicit.to_string(),
allowed: allowed_list(),
});
}
return Ok(Some(profile));
}
let normalized = normalize_base_url(query.base_url);
let mut best: Option<&ResponsesProfileBinding> = None;
for binding in BUILTIN_BINDINGS {
if !binding_matches(binding, query.provider, query.endpoint_key, &normalized) {
continue;
}
match best {
None => best = Some(binding),
Some(current) if specificity(binding) > specificity(current) => best = Some(binding),
Some(current) if specificity(binding) == specificity(current) => {
return Err(ResponsesProfileError::AmbiguousBinding {
provider: query.provider,
});
}
Some(_) => {}
}
}
let id = best
.map(|binding| binding.profile_id)
.unwrap_or(DEFAULT_PROFILE_ID);
Ok(Some(
find_profile(id.as_str()).expect("responses profile binding"),
))
}
fn find_profile(id: &str) -> Option<&'static ResponsesDialectProfile> {
BUILTIN_PROFILES
.iter()
.find(|profile| profile.id.as_str() == id)
}
fn allowed_list() -> String {
CUSTOM_ALLOWED_PROFILES
.iter()
.map(|id| id.as_str())
.collect::<Vec<_>>()
.join(", ")
}
fn binding_matches(
binding: &ResponsesProfileBinding,
provider: ProviderKind,
endpoint_key: &str,
base_url: &str,
) -> bool {
if binding.provider_kind != provider {
return false;
}
if let Some(expected) = binding.endpoint_key
&& expected != endpoint_key
{
return false;
}
if let Some(urls) = binding.base_urls
&& !urls.contains(&base_url)
{
return false;
}
true
}
fn specificity(binding: &ResponsesProfileBinding) -> u8 {
u8::from(binding.endpoint_key.is_some()) + u8::from(binding.base_urls.is_some())
}
fn normalize_base_url(url: &str) -> String {
url.trim_end_matches('/').to_ascii_lowercase()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn embedded_asset_matches_the_generated_table() {
let parsed = data::parse(include_str!("../assets/responses_dialects.json"))
.expect("responses dialect asset");
let generated = data::codegen(&parsed);
assert_eq!(parsed.profiles.len(), BUILTIN_PROFILES.len());
assert_eq!(parsed.bindings.len(), BUILTIN_BINDINGS.len());
for profile in &parsed.profiles {
assert!(
BUILTIN_PROFILES
.iter()
.any(|item| item.id.as_str() == profile.id),
"missing generated profile {}",
profile.id
);
assert!(generated.contains(&profile.id));
}
}
const DEEPSEEK_URLS: &[&str] = &[
"https://api.deepseek.com",
"https://api.deepseek.com/v1",
"https://api.deepseek.com/beta",
"https://api.deepseeki.com",
"https://api.deepseeki.com/v1",
"https://api.deepseeki.com/beta",
];
fn query<'a>(
protocol: WireFormat,
provider: ProviderKind,
endpoint_key: &'a str,
base_url: &'a str,
explicit_profile: Option<&'a str>,
) -> ResponsesProfileQuery<'a> {
ResponsesProfileQuery {
protocol,
provider,
endpoint_key,
base_url,
explicit_profile,
}
}
fn selected(query: ResponsesProfileQuery<'_>) -> &'static str {
select_responses_profile(query)
.expect("profile")
.expect("responses profile")
.id
.as_str()
}
fn asset() -> serde_json::Value {
serde_json::from_str(include_str!("../assets/responses_dialects.json")).expect("asset")
}
#[test]
fn builtin_table_has_four_profiles_and_one_usage_policy() {
let profiles = builtin_profiles();
assert_eq!(profiles.len(), 4);
let usage = profiles[0].usage;
assert!(profiles.iter().all(|profile| profile.usage == usage));
assert_eq!(
profiles
.iter()
.find(|profile| profile.id.as_str() == "codex")
.expect("codex")
.tools
.namespace,
Some("codewhale")
);
}
#[test]
fn deepseek_binding_is_the_responses_url_allowlist() {
let binding = BUILTIN_BINDINGS
.iter()
.find(|binding| binding.provider_kind == ProviderKind::Deepseek)
.expect("deepseek binding");
assert_eq!(binding.endpoint_key, Some("responses"));
assert_eq!(binding.base_urls, Some(DEEPSEEK_URLS));
assert!(
!binding
.base_urls
.expect("urls")
.contains(&"https://api.deepseek.com/anthropic")
);
}
#[test]
fn selection_follows_protocol_then_binding() {
assert!(
select_responses_profile(query(
WireFormat::ChatCompletions,
ProviderKind::OpenaiCodex,
"responses",
"https://api.openai.com/v1",
None,
))
.expect("chat")
.is_none()
);
assert_eq!(
selected(query(
WireFormat::Responses,
ProviderKind::Openai,
"responses",
"https://api.openai.com/v1",
None,
)),
"standard"
);
assert_eq!(
selected(query(
WireFormat::Responses,
ProviderKind::OpenaiCodex,
"chat",
"https://api.openai.com/v1",
None,
)),
"codex"
);
assert_eq!(
selected(query(
WireFormat::Responses,
ProviderKind::Concentrate,
"models",
"https://api.concentrate.ai/v1",
None,
)),
"concentrate"
);
for url in DEEPSEEK_URLS {
assert_eq!(
selected(query(
WireFormat::Responses,
ProviderKind::Deepseek,
"responses",
url,
None,
)),
"deepseek_stateless_plain_reasoning"
);
}
assert_eq!(
selected(query(
WireFormat::Responses,
ProviderKind::Deepseek,
"responses",
"https://API.DEEPSEEK.COM/v1/",
None,
)),
"deepseek_stateless_plain_reasoning"
);
assert_eq!(
selected(query(
WireFormat::Responses,
ProviderKind::Deepseek,
"responses",
"https://api.deepseek.com/anthropic",
None,
)),
"standard"
);
assert_eq!(
selected(query(
WireFormat::Responses,
ProviderKind::Deepseek,
"Responses",
"https://api.deepseek.com",
None,
)),
"standard"
);
}
#[test]
fn custom_override_is_allowlisted_and_built_ins_cannot_set_one() {
assert_eq!(
selected(query(
WireFormat::Responses,
ProviderKind::Custom,
"responses",
"https://gateway.example/v1",
None,
)),
"standard"
);
assert_eq!(
selected(query(
WireFormat::Responses,
ProviderKind::Custom,
"responses",
"https://gateway.example/v1",
Some("standard"),
)),
"standard"
);
let denied = select_responses_profile(query(
WireFormat::Responses,
ProviderKind::Custom,
"responses",
"https://gateway.example/v1",
Some("codex"),
))
.expect_err("codex");
assert!(denied.to_string().contains("allowed: standard"), "{denied}");
let unknown = select_responses_profile(query(
WireFormat::Responses,
ProviderKind::Custom,
"responses",
"https://gateway.example/v1",
Some("missing"),
))
.expect_err("missing");
assert!(unknown.to_string().contains("unknown responses profile"));
assert!(
select_responses_profile(query(
WireFormat::ChatCompletions,
ProviderKind::Custom,
"chat",
"https://gateway.example/v1",
Some("standard"),
))
.expect_err("wire")
.to_string()
.contains("not responses")
);
assert!(
select_responses_profile(query(
WireFormat::Responses,
ProviderKind::Openai,
"responses",
"https://api.openai.com/v1",
Some("standard"),
))
.expect_err("built-in")
.to_string()
.contains("openai")
);
}
#[test]
fn parser_rejects_unknown_fields_conflicts_and_unsafe_custom_profiles() {
let mut unknown = asset();
unknown["profiles"][0]["request"]["store"] = serde_json::json!("false");
assert!(super::data::parse(&unknown.to_string()).is_err());
let mut extra = asset();
extra["profiles"][0]["request"]["patch"] = serde_json::json!("x");
assert!(super::data::parse(&extra.to_string()).is_err());
let mut duplicate = asset();
duplicate["profiles"][1]["id"] = duplicate["profiles"][0]["id"].clone();
assert!(super::data::parse(&duplicate.to_string()).is_err());
let mut overlap = asset();
let binding = overlap["bindings"][0].clone();
overlap["bindings"]
.as_array_mut()
.expect("bindings")
.push(binding);
assert!(super::data::parse(&overlap.to_string()).is_err());
let mut custom = asset();
custom["custom_allowed_profiles"] = serde_json::json!(["standard", "codex"]);
assert!(super::data::parse(&custom.to_string()).is_err());
let mut secret = asset();
secret["profiles"][0]["request"]["api_key"] = serde_json::json!("sk-test");
assert!(super::data::parse(&secret.to_string()).is_err());
let mut urls = asset();
let deepseek = urls["bindings"]
.as_array_mut()
.expect("bindings")
.iter_mut()
.find(|binding| binding["provider_kind"] == "Deepseek")
.expect("deepseek");
deepseek["base_url_match"]["urls"]
.as_array_mut()
.expect("urls")
.push(serde_json::json!("https://api.deepseek.com/"));
assert!(super::data::parse(&urls.to_string()).is_err());
}
}
+933
View File
@@ -0,0 +1,933 @@
use std::collections::{BTreeSet, HashSet};
use serde_json::{Map, Value};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedDialects {
pub default_profile: String,
pub custom_allowed: Vec<String>,
pub profiles: Vec<ParsedProfile>,
pub bindings: Vec<ParsedBinding>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedProfile {
pub id: String,
pub endpoint_path: String,
pub protocol_headers: String,
pub store: String,
pub include: String,
pub instructions: String,
pub max_output_tokens: String,
pub sampling: String,
pub effort_mapping: String,
pub shape: String,
pub summary: String,
pub encrypted_state: String,
pub plain_reasoning: String,
pub system_role: String,
pub envelope: String,
pub namespace: Option<String>,
pub parallel_calls: String,
pub malformed_json: String,
pub unknown_event: String,
pub done_marker: String,
pub completion: String,
pub incomplete: String,
pub cache_fields: String,
pub reasoning_fields: String,
pub audio_fields: String,
pub function_tools: String,
pub server_side_tools: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedBinding {
pub provider_kind: String,
pub endpoint_key: Option<String>,
pub urls: Option<Vec<String>>,
pub profile: String,
}
const FORBIDDEN_KEYS: &[&str] = &[
"authorization",
"api_key",
"api-key",
"token",
"access_token",
"account_id",
"account-id",
"secret",
"password",
"credential",
"bearer",
];
pub fn parse(raw: &str) -> Result<ParsedDialects, String> {
let value: Value = serde_json::from_str(raw).map_err(|err| format!("invalid JSON: {err}"))?;
reject_secrets(&value, "responses dialects")?;
let root = as_object(&value, "responses dialects")?;
keys_exact(
root,
"responses dialects",
&[
"schema_version",
"default_profile",
"custom_allowed_profiles",
"profiles",
"bindings",
],
)?;
let version = root
.get("schema_version")
.and_then(Value::as_u64)
.ok_or("schema_version must be 1")?;
if version != 1 {
return Err(format!("unsupported responses dialect schema {version}"));
}
let profiles = root
.get("profiles")
.and_then(Value::as_array)
.ok_or("profiles must be an array")?;
if profiles.is_empty() {
return Err("profiles must not be empty".to_string());
}
let mut parsed_profiles = Vec::with_capacity(profiles.len());
let mut ids = BTreeSet::new();
for (index, profile) in profiles.iter().enumerate() {
let profile = parse_profile(profile, index)?;
if !ids.insert(profile.id.clone()) {
return Err(format!("duplicate profile id `{}`", profile.id));
}
parsed_profiles.push(profile);
}
let default_profile = req_str(root, "responses dialects", "default_profile")?;
if !valid_id(default_profile) {
return Err(format!("invalid default profile id `{default_profile}`"));
}
let default_profile = default_profile.to_string();
let Some(default) = parsed_profiles
.iter()
.find(|profile| profile.id == default_profile)
else {
return Err(format!(
"default profile `{default_profile}` does not exist"
));
};
if default.endpoint_path != "versioned_responses"
|| default.encrypted_state == "account_api_model_scoped"
{
return Err("default profile must be an ordinary versioned responses dialect".to_string());
}
let allowed_values = root
.get("custom_allowed_profiles")
.and_then(Value::as_array)
.ok_or("custom_allowed_profiles must be an array")?;
if allowed_values.is_empty() {
return Err("custom_allowed_profiles must not be empty".to_string());
}
let mut custom_allowed = Vec::with_capacity(allowed_values.len());
let mut seen_allowed = BTreeSet::new();
for value in allowed_values {
let id = value
.as_str()
.filter(|id| valid_id(id))
.ok_or("custom_allowed_profiles entries must be profile ids")?;
if !seen_allowed.insert(id.to_string()) {
return Err(format!("duplicate custom allowed profile `{id}`"));
}
let Some(profile) = parsed_profiles.iter().find(|profile| profile.id == id) else {
return Err(format!("custom allowed profile `{id}` does not exist"));
};
if !custom_safe(profile) {
return Err(format!(
"profile `{id}` cannot be selected by a custom provider"
));
}
custom_allowed.push(id.to_string());
}
if !custom_allowed.iter().any(|id| id == &default_profile) {
return Err("default profile must be allowed for custom providers".to_string());
}
let bindings = root
.get("bindings")
.and_then(Value::as_array)
.ok_or("bindings must be an array")?;
if bindings.is_empty() {
return Err("bindings must not be empty".to_string());
}
let mut parsed_bindings = Vec::with_capacity(bindings.len());
for (index, binding) in bindings.iter().enumerate() {
parsed_bindings.push(parse_binding(binding, index, &parsed_profiles)?);
}
for left in 0..parsed_bindings.len() {
for right in (left + 1)..parsed_bindings.len() {
if !binding_pair_ok(&parsed_bindings[left], &parsed_bindings[right]) {
return Err(format!(
"responses profile bindings overlap for {}",
parsed_bindings[left].provider_kind
));
}
}
}
for profile in &parsed_profiles {
if profile.endpoint_path != "unversioned_responses" {
continue;
}
if custom_allowed.iter().any(|id| id == &profile.id) || profile.id == default_profile {
return Err(format!(
"unversioned profile `{}` cannot be a default or custom profile",
profile.id
));
}
let bound = parsed_bindings.iter().any(|binding| {
binding.profile == profile.id
&& binding.urls.as_ref().is_some_and(|urls| !urls.is_empty())
});
if !bound {
return Err(format!(
"unversioned profile `{}` requires an exact base url binding",
profile.id
));
}
}
Ok(ParsedDialects {
default_profile,
custom_allowed,
profiles: parsed_profiles,
bindings: parsed_bindings,
})
}
pub fn codegen(doc: &ParsedDialects) -> String {
let mut out = String::from("// Generated from responses_dialects.json. Do not edit.\n");
out.push_str("const BUILTIN_PROFILES: &[ResponsesDialectProfile] = &[\n");
for profile in &doc.profiles {
out.push_str(&emit_profile(profile));
out.push_str(",\n");
}
out.push_str("];\nconst BUILTIN_BINDINGS: &[ResponsesProfileBinding] = &[\n");
for binding in &doc.bindings {
out.push_str(&emit_binding(binding));
out.push_str(",\n");
}
out.push_str(
"];\nconst DEFAULT_PROFILE_ID: ResponsesProfileId = ResponsesProfileId::from_static(",
);
out.push_str(&format!("{:?}", doc.default_profile));
out.push_str(");\nconst CUSTOM_ALLOWED_PROFILES: &[ResponsesProfileId] = &[\n");
for id in &doc.custom_allowed {
out.push_str(" ResponsesProfileId::from_static(");
out.push_str(&format!("{id:?}"));
out.push_str("),\n");
}
out.push_str("];\n");
out
}
fn emit_profile(profile: &ParsedProfile) -> String {
let namespace = match &profile.namespace {
Some(namespace) => format!("Some({namespace:?})"),
None => "None".to_string(),
};
format!(
"ResponsesDialectProfile {{
id: ResponsesProfileId::from_static({id:?}),
endpoint: ResponsesEndpointPolicy {{ path: {path} }},
headers: ResponsesHeaderPolicy {{ protocol_headers: {headers} }},
request: ResponsesRequestPolicy {{
store: {store},
include: {include},
instructions: {instructions},
max_output_tokens: {max_output_tokens},
sampling: {sampling},
}},
reasoning: ResponsesReasoningPolicy {{
effort_mapping: {effort},
shape: {shape},
summary: {summary},
}},
replay: ResponsesReplayPolicy {{
encrypted_state: {encrypted},
plain_reasoning: {plain},
system_role: {system_role},
}},
tools: ResponsesToolPolicy {{
envelope: {envelope},
namespace: {namespace},
parallel_calls: {parallel},
}},
stream: ResponsesStreamPolicy {{
malformed_json: {malformed},
unknown_event: {unknown_event},
done_marker: {done_marker},
completion: {completion},
incomplete: {incomplete},
}},
usage: ResponsesUsagePolicy {{
cache_fields: {cache_fields},
reasoning_fields: {reasoning_fields},
audio_fields: {audio_fields},
}},
capabilities: ResponsesCapabilityPolicy {{
function_tools: {function_tools},
server_side_tools: {server_side_tools},
}},
}}",
id = profile.id,
path = variant("path", &profile.endpoint_path),
headers = variant("headers", &profile.protocol_headers),
store = variant("store", &profile.store),
include = variant("include", &profile.include),
instructions = variant("instructions", &profile.instructions),
max_output_tokens = variant("max_output_tokens", &profile.max_output_tokens),
sampling = variant("sampling", &profile.sampling),
effort = variant("effort", &profile.effort_mapping),
shape = variant("shape", &profile.shape),
summary = variant("summary", &profile.summary),
encrypted = variant("encrypted", &profile.encrypted_state),
plain = variant("plain", &profile.plain_reasoning),
system_role = variant("system_role", &profile.system_role),
envelope = variant("envelope", &profile.envelope),
parallel = variant("parallel", &profile.parallel_calls),
malformed = variant("malformed", &profile.malformed_json),
unknown_event = variant("unknown_event", &profile.unknown_event),
done_marker = variant("done_marker", &profile.done_marker),
completion = variant("completion", &profile.completion),
incomplete = variant("incomplete", &profile.incomplete),
cache_fields = variant("cache_fields", &profile.cache_fields),
reasoning_fields = variant("reasoning_fields", &profile.reasoning_fields),
audio_fields = variant("audio_fields", &profile.audio_fields),
function_tools = variant("function_tools", &profile.function_tools),
server_side_tools = variant("server_side_tools", &profile.server_side_tools),
)
}
fn emit_binding(binding: &ParsedBinding) -> String {
let endpoint = match &binding.endpoint_key {
Some(endpoint) => format!("Some({endpoint:?})"),
None => "None".to_string(),
};
let urls = match &binding.urls {
Some(urls) => format!(
"Some(&[{}])",
urls.iter()
.map(|url| format!("{url:?}"))
.collect::<Vec<_>>()
.join(", ")
),
None => "None".to_string(),
};
format!(
"ResponsesProfileBinding {{
provider_kind: crate::ProviderKind::{kind},
endpoint_key: {endpoint},
base_urls: {urls},
profile_id: ResponsesProfileId::from_static({profile:?}),
}}",
kind = binding.provider_kind,
profile = binding.profile,
)
}
fn variant(group: &str, value: &str) -> &'static str {
match (group, value) {
("path", "versioned_responses") => "ResponsesPathPolicy::VersionedResponses",
("path", "unversioned_responses") => "ResponsesPathPolicy::UnversionedResponses",
("headers", "standard_json") => "ProtocolHeaderPolicy::StandardJson",
("store", "explicit_false") => "StorePolicy::ExplicitFalse",
("store", "omit") => "StorePolicy::Omit",
("include", "encrypted_reasoning") => "IncludePolicy::EncryptedReasoning",
("include", "omit") => "IncludePolicy::Omit",
("instructions", "top_level") => "InstructionsPlacement::TopLevel",
("instructions", "leading_system_input") => "InstructionsPlacement::LeadingSystemInput",
("max_output_tokens", "when_positive") => "OutputTokenFieldPolicy::WhenPositive",
("max_output_tokens", "omit") => "OutputTokenFieldPolicy::Omit",
("sampling", "temperature_and_top_p") => "SamplingPolicy::TemperatureAndTopP",
("sampling", "omit") => "SamplingPolicy::Omit",
("effort", "openai_compatible") => "ReasoningEffortMapping::OpenaiCompatible",
("effort", "codex") => "ReasoningEffortMapping::Codex",
("effort", "deepseek") => "ReasoningEffortMapping::Deepseek",
("shape", "effort_with_summary") => "ReasoningRequestShape::EffortWithSummary",
("shape", "effort_only") => "ReasoningRequestShape::EffortOnly",
("summary", "auto") => "ReasoningSummaryPolicy::Auto",
("summary", "omit") => "ReasoningSummaryPolicy::Omit",
("encrypted", "provider_api_model_scoped") => {
"EncryptedReasoningReplayPolicy::ProviderApiModelScoped"
}
("encrypted", "account_api_model_scoped") => {
"EncryptedReasoningReplayPolicy::AccountApiModelScoped"
}
("encrypted", "disabled") => "EncryptedReasoningReplayPolicy::Disabled",
("plain", "ignore") => "PlainReasoningReplayPolicy::Ignore",
("plain", "plain_text") => "PlainReasoningReplayPolicy::PlainText",
("plain", "plain_text_with_tool_placeholder") => {
"PlainReasoningReplayPolicy::PlainTextWithToolPlaceholder"
}
("system_role", "preserve") => "SystemRoleReplayPolicy::Preserve",
("system_role", "system_to_developer") => "SystemRoleReplayPolicy::SystemToDeveloper",
("envelope", "function_array") => "ToolEnvelopePolicy::FunctionArray",
("envelope", "namespace_wrapped") => "ToolEnvelopePolicy::NamespaceWrapped",
("parallel", "allow") => "ParallelToolCallPolicy::Allow",
("parallel", "disallow") => "ParallelToolCallPolicy::Disallow",
("malformed", "fail") => "MalformedEventPolicy::Fail",
("malformed", "skip_with_diagnostic") => "MalformedEventPolicy::SkipWithDiagnostic",
("unknown_event", "ignore") => "UnknownEventPolicy::Ignore",
("unknown_event", "fail") => "UnknownEventPolicy::Fail",
("done_marker", "terminal") => "DoneMarkerPolicy::Terminal",
("done_marker", "ignore") => "DoneMarkerPolicy::Ignore",
("completion", "accept_documented_terminal_events") => {
"CompletionPolicy::AcceptDocumentedTerminalEvents"
}
("completion", "require_successful_response_completed") => {
"CompletionPolicy::RequireSuccessfulResponseCompleted"
}
("incomplete", "preserve_provider_incomplete_result") => {
"IncompleteResponsePolicy::PreserveProviderIncompleteResult"
}
("incomplete", "fail") => "IncompleteResponsePolicy::Fail",
("cache_fields", "top_level_then_nested_fallback") => {
"CacheUsageFieldPolicy::TopLevelThenNestedFallback"
}
("reasoning_fields", "output_tokens_details") => {
"ReasoningUsageFieldPolicy::OutputTokensDetails"
}
("audio_fields", "nested_details") => "AudioUsageFieldPolicy::NestedDetails",
("function_tools", "supported") => "FunctionToolCapability::Supported",
("function_tools", "unsupported") => "FunctionToolCapability::Unsupported",
("server_side_tools", "route_and_adapter_gated") => {
"ServerSideToolCapability::RouteAndAdapterGated"
}
("server_side_tools", "unsupported") => "ServerSideToolCapability::Unsupported",
_ => unreachable!("validator accepted an unknown responses dialect value"),
}
}
fn parse_profile(value: &Value, index: usize) -> Result<ParsedProfile, String> {
let at = format!("profiles[{index}]");
let map = as_object(value, &at)?;
keys_exact(
map,
&at,
&[
"id",
"endpoint",
"headers",
"request",
"reasoning",
"replay",
"tools",
"stream",
"usage",
"capabilities",
],
)?;
let id = req_str(map, &at, "id")?;
if !valid_id(id) {
return Err(format!("{at} has invalid id `{id}`"));
}
let endpoint = nested(map, &at, "endpoint", &["path"])?;
let headers = nested(map, &at, "headers", &["protocol_headers"])?;
let request = nested(
map,
&at,
"request",
&[
"store",
"include",
"instructions",
"max_output_tokens",
"sampling",
],
)?;
let reasoning = nested(
map,
&at,
"reasoning",
&["effort_mapping", "shape", "summary"],
)?;
let replay = nested(
map,
&at,
"replay",
&["encrypted_state", "plain_reasoning", "system_role"],
)?;
let tools = nested(
map,
&at,
"tools",
&["envelope", "namespace", "parallel_calls"],
)?;
let stream = nested(
map,
&at,
"stream",
&[
"malformed_json",
"unknown_event",
"done_marker",
"completion",
"incomplete",
],
)?;
let usage = nested(
map,
&at,
"usage",
&["cache_fields", "reasoning_fields", "audio_fields"],
)?;
let capabilities = nested(
map,
&at,
"capabilities",
&["function_tools", "server_side_tools"],
)?;
let endpoint_path = enum_str(
&endpoint,
&format!("{at}.endpoint"),
"path",
&["versioned_responses", "unversioned_responses"],
)?;
let shape = enum_str(
&reasoning,
&format!("{at}.reasoning"),
"shape",
&["effort_with_summary", "effort_only"],
)?;
let summary = enum_str(
&reasoning,
&format!("{at}.reasoning"),
"summary",
&["auto", "omit"],
)?;
match (shape, summary) {
("effort_with_summary", "auto") | ("effort_only", "omit") => {}
_ => {
return Err(format!(
"{at} reasoning shape `{shape}` does not match summary `{summary}`"
));
}
}
let envelope = enum_str(
&tools,
&format!("{at}.tools"),
"envelope",
&["function_array", "namespace_wrapped"],
)?;
let namespace = optional_namespace(&tools, &format!("{at}.tools"))?;
match (envelope, namespace.as_deref()) {
("namespace_wrapped", Some(namespace)) if valid_id(namespace) => {}
("function_array", None) => {}
_ => {
return Err(format!(
"{at} tool namespace does not match envelope `{envelope}`"
));
}
}
let encrypted_state = enum_str(
&replay,
&format!("{at}.replay"),
"encrypted_state",
&[
"provider_api_model_scoped",
"account_api_model_scoped",
"disabled",
],
)?;
Ok(ParsedProfile {
id: id.to_string(),
endpoint_path: endpoint_path.to_string(),
protocol_headers: enum_str(
&headers,
&format!("{at}.headers"),
"protocol_headers",
&["standard_json"],
)?
.to_string(),
store: enum_str(
&request,
&format!("{at}.request"),
"store",
&["explicit_false", "omit"],
)?
.to_string(),
include: enum_str(
&request,
&format!("{at}.request"),
"include",
&["encrypted_reasoning", "omit"],
)?
.to_string(),
instructions: enum_str(
&request,
&format!("{at}.request"),
"instructions",
&["top_level", "leading_system_input"],
)?
.to_string(),
max_output_tokens: enum_str(
&request,
&format!("{at}.request"),
"max_output_tokens",
&["when_positive", "omit"],
)?
.to_string(),
sampling: enum_str(
&request,
&format!("{at}.request"),
"sampling",
&["temperature_and_top_p", "omit"],
)?
.to_string(),
effort_mapping: enum_str(
&reasoning,
&format!("{at}.reasoning"),
"effort_mapping",
&["openai_compatible", "codex", "deepseek"],
)?
.to_string(),
shape: shape.to_string(),
summary: summary.to_string(),
encrypted_state: encrypted_state.to_string(),
plain_reasoning: enum_str(
&replay,
&format!("{at}.replay"),
"plain_reasoning",
&["ignore", "plain_text", "plain_text_with_tool_placeholder"],
)?
.to_string(),
system_role: enum_str(
&replay,
&format!("{at}.replay"),
"system_role",
&["preserve", "system_to_developer"],
)?
.to_string(),
envelope: envelope.to_string(),
namespace,
parallel_calls: enum_str(
&tools,
&format!("{at}.tools"),
"parallel_calls",
&["allow", "disallow"],
)?
.to_string(),
malformed_json: enum_str(
&stream,
&format!("{at}.stream"),
"malformed_json",
&["fail", "skip_with_diagnostic"],
)?
.to_string(),
unknown_event: enum_str(
&stream,
&format!("{at}.stream"),
"unknown_event",
&["ignore", "fail"],
)?
.to_string(),
done_marker: enum_str(
&stream,
&format!("{at}.stream"),
"done_marker",
&["terminal", "ignore"],
)?
.to_string(),
completion: enum_str(
&stream,
&format!("{at}.stream"),
"completion",
&[
"accept_documented_terminal_events",
"require_successful_response_completed",
],
)?
.to_string(),
incomplete: enum_str(
&stream,
&format!("{at}.stream"),
"incomplete",
&["preserve_provider_incomplete_result", "fail"],
)?
.to_string(),
cache_fields: enum_str(
&usage,
&format!("{at}.usage"),
"cache_fields",
&["top_level_then_nested_fallback"],
)?
.to_string(),
reasoning_fields: enum_str(
&usage,
&format!("{at}.usage"),
"reasoning_fields",
&["output_tokens_details"],
)?
.to_string(),
audio_fields: enum_str(
&usage,
&format!("{at}.usage"),
"audio_fields",
&["nested_details"],
)?
.to_string(),
function_tools: enum_str(
&capabilities,
&format!("{at}.capabilities"),
"function_tools",
&["supported", "unsupported"],
)?
.to_string(),
server_side_tools: enum_str(
&capabilities,
&format!("{at}.capabilities"),
"server_side_tools",
&["route_and_adapter_gated", "unsupported"],
)?
.to_string(),
})
}
fn parse_binding(
value: &Value,
index: usize,
profiles: &[ParsedProfile],
) -> Result<ParsedBinding, String> {
let at = format!("bindings[{index}]");
let map = as_object(value, &at)?;
keys_exact(
map,
&at,
&[
"provider_kind",
"protocol",
"endpoint_key",
"base_url_match",
"profile",
],
)?;
let provider_kind = req_str(map, &at, "provider_kind")?;
if !provider_kind.chars().all(|ch| ch.is_ascii_alphanumeric()) {
return Err(format!("{at} has invalid provider kind `{provider_kind}`"));
}
let protocol = req_str(map, &at, "protocol")?;
if protocol != "responses" {
return Err(format!("{at} protocol must be responses"));
}
let endpoint_key = match map.get("endpoint_key") {
None => None,
Some(Value::String(endpoint)) if valid_id(endpoint.as_str()) => Some(endpoint.clone()),
_ => return Err(format!("{at}.endpoint_key must be a profile-style id")),
};
let profile = req_str(map, &at, "profile")?;
if !profiles.iter().any(|item| item.id == profile) {
return Err(format!("{at} references unknown profile `{profile}`"));
}
let urls = parse_url_match(map.get("base_url_match").unwrap_or(&Value::Null), &at)?;
Ok(ParsedBinding {
provider_kind: provider_kind.to_string(),
endpoint_key,
urls,
profile: profile.to_string(),
})
}
fn parse_url_match(value: &Value, at: &str) -> Result<Option<Vec<String>>, String> {
let at = format!("{at}.base_url_match");
let map = as_object(value, &at)?;
let kind = req_str(map, &at, "type")?;
match kind {
"any" => {
keys_exact(map, &at, &["type"])?;
Ok(None)
}
"exact_normalized" => {
keys_exact(map, &at, &["type", "urls"])?;
let urls = map
.get("urls")
.and_then(Value::as_array)
.ok_or_else(|| format!("{at}.urls must be an array"))?;
if urls.is_empty() {
return Err(format!("{at}.urls must not be empty"));
}
let mut parsed = Vec::with_capacity(urls.len());
let mut seen = HashSet::new();
for url in urls {
let url = url
.as_str()
.ok_or_else(|| format!("{at}.urls entries must be strings"))?;
if url.is_empty()
|| url
.chars()
.any(|ch| ch.is_whitespace() || matches!(ch, '?' | '#' | '@'))
|| !url.starts_with("https://")
|| url != url.trim_end_matches('/').to_ascii_lowercase()
{
return Err(format!(
"{at} url `{url}` is not an exact normalized https url"
));
}
if !seen.insert(url.to_string()) {
return Err(format!("{at} repeats url `{url}`"));
}
parsed.push(url.to_string());
}
Ok(Some(parsed))
}
_ => Err(format!("{at} has unknown match `{kind}`")),
}
}
fn custom_safe(profile: &ParsedProfile) -> bool {
profile.encrypted_state != "account_api_model_scoped"
&& profile.endpoint_path != "unversioned_responses"
&& profile.envelope != "namespace_wrapped"
}
fn binding_pair_ok(left: &ParsedBinding, right: &ParsedBinding) -> bool {
if !bindings_overlap(left, right) {
return true;
}
strictly_more_specific(left, right) || strictly_more_specific(right, left)
}
fn bindings_overlap(left: &ParsedBinding, right: &ParsedBinding) -> bool {
if left.provider_kind != right.provider_kind {
return false;
}
if let (Some(left_endpoint), Some(right_endpoint)) = (&left.endpoint_key, &right.endpoint_key)
&& left_endpoint != right_endpoint
{
return false;
}
match (&left.urls, &right.urls) {
(Some(left_urls), Some(right_urls)) => left_urls.iter().any(|url| right_urls.contains(url)),
_ => true,
}
}
fn strictly_more_specific(specific: &ParsedBinding, general: &ParsedBinding) -> bool {
specificity(specific) > specificity(general) && constraints_fit(specific, general)
}
fn specificity(binding: &ParsedBinding) -> u8 {
u8::from(binding.endpoint_key.is_some()) + u8::from(binding.urls.is_some())
}
fn constraints_fit(specific: &ParsedBinding, general: &ParsedBinding) -> bool {
if let Some(endpoint) = &general.endpoint_key
&& specific.endpoint_key.as_ref() != Some(endpoint)
{
return false;
}
if let Some(general_urls) = &general.urls {
let Some(specific_urls) = &specific.urls else {
return false;
};
if specific_urls.iter().any(|url| !general_urls.contains(url)) {
return false;
}
}
true
}
fn nested(
map: &Map<String, Value>,
at: &str,
key: &str,
fields: &[&str],
) -> Result<Map<String, Value>, String> {
let value = map
.get(key)
.ok_or_else(|| format!("{at} is missing `{key}`"))?;
let child = as_object(value, &format!("{at}.{key}"))?;
keys_exact(child, &format!("{at}.{key}"), fields)?;
Ok(child.clone())
}
fn optional_namespace(map: &Map<String, Value>, at: &str) -> Result<Option<String>, String> {
match map.get("namespace") {
None => Err(format!("{at} is missing `namespace`")),
Some(Value::Null) => Ok(None),
Some(Value::String(namespace)) if !namespace.is_empty() => Ok(Some(namespace.clone())),
_ => Err(format!("{at}.namespace must be a string or null")),
}
}
fn enum_str<'a>(
map: &'a Map<String, Value>,
at: &str,
key: &str,
allowed: &[&str],
) -> Result<&'a str, String> {
let value = req_str(map, at, key)?;
if allowed.contains(&value) {
Ok(value)
} else {
Err(format!("{at}.{key} has unknown value `{value}`"))
}
}
fn req_str<'a>(map: &'a Map<String, Value>, at: &str, key: &str) -> Result<&'a str, String> {
match map.get(key) {
Some(Value::String(value)) if !value.is_empty() => Ok(value),
Some(_) => Err(format!("{at}.{key} must be a non-empty string")),
None => Err(format!("{at} is missing `{key}`")),
}
}
fn as_object<'a>(value: &'a Value, at: &str) -> Result<&'a Map<String, Value>, String> {
value
.as_object()
.ok_or_else(|| format!("{at} must be an object"))
}
fn keys_exact(map: &Map<String, Value>, at: &str, allowed: &[&str]) -> Result<(), String> {
for key in map.keys() {
if !allowed.contains(&key.as_str()) {
return Err(format!("{at} has unknown field `{key}`"));
}
}
Ok(())
}
fn valid_id(value: &str) -> bool {
let mut chars = value.chars();
match chars.next() {
Some(first) if first.is_ascii_lowercase() => {}
_ => return false,
}
chars.all(|ch| ch.is_ascii_lowercase() || ch.is_ascii_digit() || ch == '_')
}
fn reject_secrets(value: &Value, at: &str) -> Result<(), String> {
match value {
Value::Object(map) => {
for (key, child) in map {
if FORBIDDEN_KEYS
.iter()
.any(|forbidden| key.eq_ignore_ascii_case(forbidden))
{
return Err(format!("{at}.{key} is not allowed in a responses profile"));
}
reject_secrets(child, &format!("{at}.{key}"))?;
}
Ok(())
}
Value::Array(items) => {
for (index, child) in items.iter().enumerate() {
reject_secrets(child, &format!("{at}[{index}]"))?;
}
Ok(())
}
Value::String(text) => {
let lower = text.to_ascii_lowercase();
if lower.contains("bearer ") || lower.starts_with("sk-") {
Err(format!("{at} contains a credential-like value"))
} else {
Ok(())
}
}
_ => Ok(()),
}
}
+1
View File
@@ -213,6 +213,7 @@ mod tests {
base_url_override: None,
limit_overrides: Vec::new(),
wire_override: None,
responses_profile_override: None,
}
}
+18
View File
@@ -25,6 +25,7 @@ use super::capabilities::RouteCapabilities;
use super::ids::{LogicalModelRef, ModelId, ProviderId, WireModelId};
use super::offering::RouteLimits;
use crate::ProviderKind;
use crate::responses_dialect::ResponsesProfileId;
/// A concrete, resolved endpoint the route will talk to.
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -202,6 +203,10 @@ pub struct ReadyRouteCandidate {
auth: ResolvedAuthSource,
/// Selected wire protocol.
protocol: RequestProtocol,
/// Responses dialect selected after the protocol is known.
/// Absent for every non-Responses route.
#[serde(skip_serializing_if = "Option::is_none")]
responses_profile_id: Option<ResponsesProfileId>,
/// Route/offering-scoped token limits, when known (overrides applied).
limits: RouteLimits,
/// Capability facts for the exact provider/model offering.
@@ -228,12 +233,18 @@ impl ReadyRouteCandidate {
endpoint: ResolvedEndpoint,
auth: ResolvedAuthSource,
protocol: RequestProtocol,
responses_profile_id: Option<ResponsesProfileId>,
limits: RouteLimits,
capabilities: RouteCapabilities,
pricing: Option<PricingSku>,
validation: ValidationReport,
applied_limit_overrides: Vec<SourcedLimitOverride>,
) -> Self {
assert_eq!(
protocol == RequestProtocol::Responses,
responses_profile_id.is_some(),
"a responses route carries a profile id and other routes do not"
);
Self {
provider_id,
provider_kind,
@@ -243,6 +254,7 @@ impl ReadyRouteCandidate {
endpoint,
auth,
protocol,
responses_profile_id,
limits,
capabilities,
pricing,
@@ -299,6 +311,12 @@ impl ReadyRouteCandidate {
self.protocol
}
/// Responses dialect id, present exactly when the protocol is Responses.
#[must_use]
pub fn responses_profile_id(&self) -> Option<ResponsesProfileId> {
self.responses_profile_id
}
/// Route/offering-scoped token limits, when known (overrides applied).
#[must_use]
pub fn limits(&self) -> RouteLimits {
@@ -20,6 +20,7 @@ fn none_request(kind: ProviderKind) -> RouteRequest {
base_url_override: None,
limit_overrides: Vec::new(),
wire_override: None,
responses_profile_override: None,
}
}
@@ -127,6 +128,7 @@ fn every_provider_kind_resolves_the_auto_selector() {
base_url_override: None,
limit_overrides: Vec::new(),
wire_override: None,
responses_profile_override: None,
};
let candidate = resolver
.resolve(&request)
+12
View File
@@ -23,6 +23,13 @@ pub enum RouteError {
/// The foreign model selector that was rejected.
model: String,
},
/// The resolved route has no unambiguous Responses dialect profile.
ResponsesProfile {
/// Provider whose route was being resolved.
provider: ProviderId,
/// Profile selection failure.
detail: String,
},
/// A model-aware provider did not prove a supported request protocol for
/// the selected model/endpoint.
UnsupportedModelProtocol {
@@ -53,6 +60,11 @@ impl fmt::Display for RouteError {
"model {model:?} is not served by direct provider {}",
provider.as_str()
),
Self::ResponsesProfile { provider, detail } => write!(
f,
"provider {} responses profile: {detail}",
provider.as_str()
),
Self::UnsupportedModelProtocol {
provider,
model,
+19
View File
@@ -70,6 +70,10 @@ pub struct RouteRequest {
/// `wire = "responses"` mints a wire-true candidate instead of Custom's
/// backward-compatible static Chat policy.
pub wire_override: Option<WireFormat>,
/// Explicit Responses profile from a custom provider table. Built-in
/// routes leave this empty and take the binding selected after the
/// protocol is known. `None` on a Responses route uses the default profile.
pub responses_profile_override: Option<String>,
}
/// Resolves [`RouteRequest`]s into [`ReadyRouteCandidate`]s.
@@ -541,6 +545,20 @@ impl RouteResolver {
.push("endpoint uses insecure http:// (credentials sent in plaintext)".to_string());
}
let validation = ValidationReport { ok: true, messages };
let responses_profile_id = crate::responses_dialect::select_responses_profile(
crate::responses_dialect::ResponsesProfileQuery {
protocol,
provider: provider_kind,
endpoint_key: &endpoint.endpoint_key,
base_url: &endpoint.base_url,
explicit_profile: req.responses_profile_override.as_deref(),
},
)
.map_err(|err| RouteError::ResponsesProfile {
provider: provider_id.clone(),
detail: err.to_string(),
})?
.map(|profile| profile.id);
// Apply caller-requested limit overrides in order, BEFORE the candidate
// is minted. The candidate is immutable afterwards; the applied
@@ -570,6 +588,7 @@ impl RouteResolver {
// `Unresolved` at resolution time, not a claimed `Missing`.
ResolvedAuthSource::Unresolved,
protocol,
responses_profile_id,
limits,
selected.capabilities,
// #3085: honest pricing projected from the matched offering (the
+90
View File
@@ -20,6 +20,7 @@ fn req(provider: Option<ProviderKind>, model: Option<&str>) -> RouteRequest {
base_url_override: None,
limit_overrides: Vec::new(),
wire_override: None,
responses_profile_override: None,
}
}
@@ -86,6 +87,11 @@ fn concentrate_passes_ids_through_and_strips_only_its_own_namespace() {
RequestProtocol::Responses,
"{raw} must ride the Responses wire"
);
assert_eq!(
out.responses_profile_id().map(|id| id.as_str()),
Some("concentrate"),
"{raw} profile"
);
}
// Codewhale's own `auto` sentinel resolves to the provider default, never
@@ -266,6 +272,12 @@ fn resolver_records_exact_limit_override_provenance() {
.expect("Codex route with sourced overrides resolves");
assert_eq!(candidate.applied_limit_overrides(), overrides.as_slice());
assert_eq!(candidate.protocol(), RequestProtocol::Responses);
assert_eq!(
candidate.responses_profile_id().map(|id| id.as_str()),
Some("codex")
);
assert_ne!(candidate.endpoint().endpoint_key, "responses");
}
#[test]
@@ -395,6 +407,10 @@ fn resolver_routes_only_official_deepseek_flash_over_responses() {
.expect("official default Flash route resolves");
assert_eq!(default_flash.protocol(), RequestProtocol::Responses);
assert_eq!(default_flash.endpoint().endpoint_key, "responses");
assert_eq!(
default_flash.responses_profile_id().map(|id| id.as_str()),
Some("deepseek_stateless_plain_reasoning")
);
assert_eq!(
default_flash.capabilities().server_side_web_search,
CapabilityState::Supported
@@ -408,6 +424,10 @@ fn resolver_routes_only_official_deepseek_flash_over_responses() {
.expect("official Flash route resolves");
assert_eq!(flash.protocol(), RequestProtocol::Responses);
assert_eq!(flash.endpoint().endpoint_key, "responses");
assert_eq!(
flash.responses_profile_id().map(|id| id.as_str()),
Some("deepseek_stateless_plain_reasoning")
);
assert_eq!(
flash.capabilities().server_side_web_search,
CapabilityState::Supported
@@ -418,6 +438,7 @@ fn resolver_routes_only_official_deepseek_flash_over_responses() {
.expect("official Pro route resolves");
assert_eq!(pro.protocol(), RequestProtocol::ChatCompletions);
assert_eq!(pro.endpoint().endpoint_key, "chat");
assert!(pro.responses_profile_id().is_none());
assert_eq!(
pro.capabilities().server_side_web_search,
CapabilityState::Supported
@@ -431,6 +452,7 @@ fn resolver_routes_only_official_deepseek_flash_over_responses() {
.expect("unknown future model preserves direct-provider pass-through");
assert_eq!(future.protocol(), RequestProtocol::ChatCompletions);
assert_eq!(future.endpoint().endpoint_key, "chat");
assert!(future.responses_profile_id().is_none());
let legacy_unknown = resolver
.resolve(&req(
@@ -440,6 +462,7 @@ fn resolver_routes_only_official_deepseek_flash_over_responses() {
.expect("unknown unversioned model preserves historical Chat pass-through");
assert_eq!(legacy_unknown.protocol(), RequestProtocol::ChatCompletions);
assert_eq!(legacy_unknown.endpoint().endpoint_key, "chat");
assert!(legacy_unknown.responses_profile_id().is_none());
let custom = resolver
.resolve(&RouteRequest {
@@ -449,10 +472,12 @@ fn resolver_routes_only_official_deepseek_flash_over_responses() {
base_url_override: Some("https://compatible.example/v1".to_string()),
limit_overrides: Vec::new(),
wire_override: None,
responses_profile_override: None,
})
.expect("custom compatible Flash route remains pass-through");
assert_eq!(custom.protocol(), RequestProtocol::ChatCompletions);
assert_eq!(custom.endpoint().endpoint_key, "chat");
assert!(custom.responses_profile_id().is_none());
assert_eq!(
custom.capabilities().server_side_web_search,
CapabilityState::Unknown
@@ -497,6 +522,7 @@ fn resolver_sources_deepseek_files_api_only_on_exact_official_flash_routes() {
base_url_override: Some("https://compatible.example/v1".to_string()),
limit_overrides: Vec::new(),
wire_override: None,
responses_profile_override: None,
})
.expect("custom compatible endpoint resolves");
assert_eq!(custom.capabilities().files_api, CapabilityState::Unknown);
@@ -556,6 +582,7 @@ fn resolver_routes_deepseek_vision_exp_over_chat_with_image_input() {
base_url_override: base_url_override.map(str::to_string),
limit_overrides: Vec::new(),
wire_override: None,
responses_profile_override: None,
})
.expect("experimental vision route resolves");
@@ -668,6 +695,7 @@ fn resolver_keeps_custom_deepseek_same_name_capabilities_unverified() {
base_url_override: Some("https://deepseek-proxy.example.test/v1".to_string()),
limit_overrides: Vec::new(),
wire_override: None,
responses_profile_override: None,
})
.expect("same-name custom proxy route resolves");
@@ -952,6 +980,7 @@ fn resolver_direct_owned_row_match_survives_casing_mismatch() {
base_url_override: Some("https://compatible.example.test/v1".to_string()),
limit_overrides: Vec::new(),
wire_override: None,
responses_profile_override: None,
};
let out = r
.resolve(&custom)
@@ -1019,6 +1048,7 @@ fn resolver_custom_endpoint_allows_namespaced_selector_for_strict_provider() {
base_url_override: Some("https://example.local/v1".to_string()),
limit_overrides: Vec::new(),
wire_override: None,
responses_profile_override: None,
};
let out = r
.resolve(&request)
@@ -1043,6 +1073,7 @@ fn resolver_treats_every_official_deepseek_endpoint_as_strict_direct() {
base_url_override: Some(base_url.to_string()),
limit_overrides: Vec::new(),
wire_override: None,
responses_profile_override: None,
};
assert!(
matches!(
@@ -1064,6 +1095,7 @@ fn resolver_does_not_trust_deepseek_hostname_substrings() {
base_url_override: Some("https://api.deepseek.com.evil.example/v1".to_string()),
limit_overrides: Vec::new(),
wire_override: None,
responses_profile_override: None,
};
let route = resolver
.resolve(&request)
@@ -1084,6 +1116,7 @@ fn resolver_explicit_custom_with_base_url_override_passes_model_through_verbatim
base_url_override: Some("https://api.example.com/v1".to_string()),
limit_overrides: Vec::new(),
wire_override: None,
responses_profile_override: None,
};
let out = r
.resolve(&request)
@@ -1096,6 +1129,7 @@ fn resolver_explicit_custom_with_base_url_override_passes_model_through_verbatim
out.protocol(),
crate::route::RequestProtocol::ChatCompletions
);
assert!(out.responses_profile_id().is_none());
assert!(out.validation().ok);
assert!(out.validation().messages.is_empty());
}
@@ -1125,11 +1159,17 @@ fn custom_wire_override_mints_a_wire_true_candidate() {
base_url_override: Some("https://relay.example.com/v1".to_string()),
limit_overrides: Vec::new(),
wire_override: Some(dialect),
responses_profile_override: None,
};
let out = r
.resolve(&request)
.expect("custom provider should resolve via pass-through");
assert_eq!(out.protocol(), protocol, "{dialect:?} must mint its wire");
assert_eq!(
out.responses_profile_id().map(|id| id.as_str()),
(protocol == RequestProtocol::Responses).then_some("standard"),
"{dialect:?} profile"
);
assert_eq!(out.endpoint().base_url, "https://relay.example.com/v1");
assert_eq!(out.wire_model_id().as_str(), "gpt-5.5");
assert!(out.validation().ok);
@@ -1138,6 +1178,40 @@ fn custom_wire_override_mints_a_wire_true_candidate() {
}
}
#[test]
fn custom_responses_profile_override_stays_on_the_allowlist() {
let resolver = RouteResolver::new();
let mut request = RouteRequest {
explicit_provider: Some(ProviderKind::Custom),
model_selector: Some(LogicalModelRef::from("gpt-5.5")),
saved_provider_model: None,
base_url_override: Some("https://relay.example.com/v1".to_string()),
limit_overrides: Vec::new(),
wire_override: Some(crate::provider::WireFormat::Responses),
responses_profile_override: Some("standard".to_string()),
};
let standard = resolver.resolve(&request).expect("standard profile");
assert_eq!(
standard.responses_profile_id().map(|id| id.as_str()),
Some("standard")
);
request.responses_profile_override = None;
let omitted = resolver.resolve(&request).expect("default profile");
assert_eq!(
omitted.responses_profile_id().map(|id| id.as_str()),
Some("standard")
);
request.responses_profile_override = Some("codex".to_string());
let denied = resolver.resolve(&request).expect_err("codex is not custom");
assert!(denied.to_string().contains("allowed: standard"), "{denied}");
request.wire_override = None;
request.responses_profile_override = Some("standard".to_string());
let chat = resolver
.resolve(&request)
.expect_err("profile requires responses");
assert!(chat.to_string().contains("not responses"), "{chat}");
}
#[test]
fn wire_override_is_ignored_for_builtin_kinds() {
// Built-ins keep their descriptor policy: a caller-supplied override can
@@ -1150,6 +1224,7 @@ fn wire_override_is_ignored_for_builtin_kinds() {
base_url_override: None,
limit_overrides: Vec::new(),
wire_override: Some(crate::provider::WireFormat::Responses),
responses_profile_override: None,
};
let out = r.resolve(&request).expect("deepseek resolves");
assert_eq!(
@@ -1172,6 +1247,7 @@ fn custom_wire_override_defaults_to_the_chat_policy() {
base_url_override: Some("https://api.example.com/v1".to_string()),
limit_overrides: Vec::new(),
wire_override,
responses_profile_override: None,
};
let out = r.resolve(&request).expect("custom resolves");
assert_eq!(
@@ -1318,6 +1394,7 @@ fn together_custom_endpoint_preserves_its_explicit_model_id() {
base_url_override: Some("http://127.0.0.1:8000/v1".to_string()),
limit_overrides: Vec::new(),
wire_override: None,
responses_profile_override: None,
})
.expect("custom Together-compatible endpoint should resolve");
@@ -1354,6 +1431,7 @@ fn openrouter_custom_endpoint_preserves_qwen37_alias() {
base_url_override: Some("https://gateway.example.test/v1".to_string()),
limit_overrides: Vec::new(),
wire_override: None,
responses_profile_override: None,
})
.expect("custom OpenRouter-compatible endpoint should resolve");
@@ -1633,6 +1711,7 @@ fn opencode_zen_resolver_fails_closed_for_unproven_protocols() {
base_url_override,
limit_overrides: Vec::new(),
wire_override: None,
responses_profile_override: None,
};
assert!(
matches!(
@@ -1753,6 +1832,7 @@ fn resolver_empty_saved_provider_model_is_empty_model_error() {
base_url_override: None,
limit_overrides: Vec::new(),
wire_override: None,
responses_profile_override: None,
};
assert!(matches!(r.resolve(&request), Err(RouteError::EmptyModel)));
}
@@ -1950,6 +2030,7 @@ fn provider_native_web_search_requires_exact_direct_endpoint_offering() {
base_url_override: Some("https://gateway.example.test/v1".to_string()),
limit_overrides: Vec::new(),
wire_override: None,
responses_profile_override: None,
})
.expect("custom compatible endpoint resolves");
assert_eq!(
@@ -1992,6 +2073,7 @@ fn mimo_native_search_is_exact_to_documented_chat_models() {
base_url_override: Some("https://compatible.example.test/v1".to_string()),
limit_overrides: Vec::new(),
wire_override: None,
responses_profile_override: None,
})
.expect("custom MiMo-compatible route resolves");
assert_eq!(
@@ -2017,6 +2099,7 @@ fn zai_native_search_requires_exact_general_api_product() {
base_url_override: Some(base_url.to_string()),
limit_overrides: Vec::new(),
wire_override: None,
responses_profile_override: None,
})
.expect("general API route resolves");
assert_eq!(
@@ -2039,6 +2122,7 @@ fn zai_native_search_requires_exact_general_api_product() {
base_url_override: Some(base_url.to_string()),
limit_overrides: Vec::new(),
wire_override: None,
responses_profile_override: None,
})
.expect("adjacent route resolves");
assert_eq!(
@@ -2089,6 +2173,7 @@ fn qwen_native_search_is_exact_to_token_plan_responses_routes() {
base_url_override: Some(base_url.to_string()),
limit_overrides: Vec::new(),
wire_override: None,
responses_profile_override: None,
})
.expect("alternate product route resolves");
assert_eq!(
@@ -2119,6 +2204,7 @@ fn moonshot_native_search_requires_exact_product_model_pair() {
base_url_override: Some(base_url.to_string()),
limit_overrides: Vec::new(),
wire_override: None,
responses_profile_override: None,
})
.expect("documented Moonshot/Kimi route resolves");
assert_eq!(
@@ -2142,6 +2228,7 @@ fn moonshot_native_search_requires_exact_product_model_pair() {
base_url_override: Some(base_url.to_string()),
limit_overrides: Vec::new(),
wire_override: None,
responses_profile_override: None,
})
.expect("adjacent Moonshot/Kimi route resolves");
assert_eq!(
@@ -2185,6 +2272,7 @@ fn custom_endpoint_does_not_inherit_first_party_pricing() {
base_url_override: Some("https://deepseek-proxy.example.test/v1".to_string()),
limit_overrides: Vec::new(),
wire_override: None,
responses_profile_override: None,
})
.expect("same-name custom proxy route resolves");
@@ -2206,6 +2294,7 @@ fn exact_endpoint_catalog_may_carry_its_own_pricing_on_a_custom_base_url() {
base_url_override: Some("https://authenticated-catalog.example.test/v1".to_string()),
limit_overrides: Vec::new(),
wire_override: None,
responses_profile_override: None,
};
let out = priced_deepseek_resolver()
.resolve_with_endpoint_catalog_authority(&request)
@@ -2263,6 +2352,7 @@ fn req_with_base(provider: ProviderKind, model: &str, base_url: &str) -> RouteRe
base_url_override: Some(base_url.to_string()),
limit_overrides: Vec::new(),
wire_override: None,
responses_profile_override: None,
}
}
+1
View File
@@ -5705,6 +5705,7 @@ model = "gpt-5.5"
base_url_override: None,
limit_overrides: Vec::new(),
wire_override: None,
responses_profile_override: None,
})
.expect("documented Zen model must resolve");
assert_eq!(route.protocol(), crate::route::RequestProtocol::Responses);
+118 -26
View File
@@ -32,6 +32,7 @@ use codewhale_config::catalog::{
#[cfg(any(test, feature = "test-support"))]
use codewhale_config::catalog::{CatalogSnapshot, CatalogStatus, ProviderCatalogCache};
use codewhale_config::provider::WireFormat;
use codewhale_config::responses_dialect::{ResponsesPathPolicy, ResponsesProfileId};
use codewhale_config::route::{
LogicalModelRef, ReadyRouteCandidate, RouteLimits, RouteRequest, RouteResolver,
};
@@ -282,6 +283,8 @@ pub struct CodewhaleClient {
/// token refresh and absent on custom API-key routes.
pub(super) chatgpt_reasoning_api: Option<String>,
wire_format: WireFormat,
/// Admitted Responses dialect. Empty for Chat, Messages, and catalog probes.
responses_profile_id: Option<ResponsesProfileId>,
retry: RetryPolicy,
/// Auxiliary inspection calls use the normal bounded retry schedule but
/// never publish retry/rate-limit state into process-global UI cells.
@@ -589,6 +592,7 @@ impl Clone for CodewhaleClient {
codex_account_id: self.codex_account_id.clone(),
chatgpt_reasoning_api: self.chatgpt_reasoning_api.clone(),
wire_format: self.wire_format,
responses_profile_id: self.responses_profile_id,
retry: self.retry.clone(),
isolated_request_state: self.isolated_request_state,
remote_control_inference_participant: self.remote_control_inference_participant,
@@ -1196,22 +1200,12 @@ pub fn api_url(base_url: &str, path: &str) -> String {
api_url_with_suffix(base_url, path, None)
}
fn responses_api_url(base_url: &str, provider: ProviderKind) -> String {
let normalized = base_url.trim_end_matches('/').to_ascii_lowercase();
let official_deepseek = matches!(provider, ProviderKind::Deepseek)
&& matches!(
normalized.as_str(),
"https://api.deepseek.com"
| "https://api.deepseek.com/v1"
| "https://api.deepseek.com/beta"
| "https://api.deepseeki.com"
| "https://api.deepseeki.com/v1"
| "https://api.deepseeki.com/beta"
);
if official_deepseek {
format!("{}/responses", unversioned_base_url(base_url))
} else {
api_url(base_url, "responses")
fn responses_api_url(base_url: &str, path: ResponsesPathPolicy) -> String {
match path {
ResponsesPathPolicy::UnversionedResponses => {
format!("{}/responses", unversioned_base_url(base_url))
}
ResponsesPathPolicy::VersionedResponses => api_url(base_url, "responses"),
}
}
@@ -1518,20 +1512,31 @@ impl CodewhaleClient {
return Self::from_candidate(&route.config, &route.candidate)
.map(|client| client.with_context_window(route.context_window));
}
let resolved = crate::route_runtime::resolve_runtime_route_for_identity(
let wire_format = provider_wire_format_for_config(&identity, Some(config));
let resolved = match crate::route_runtime::resolve_runtime_route_for_identity(
config,
&identity,
Some(&default_model),
)
.ok();
) {
Ok(route) => Some(route),
Err(error) if wire_format == WireFormat::Responses => {
return Err(anyhow::Error::msg(error));
}
Err(_) => None,
};
let route_limits = resolved
.as_ref()
.and_then(|route| crate::route_budget::known_route_limits(route.candidate.limits()));
let responses_profile_id = resolved
.as_ref()
.and_then(|route| route.candidate.responses_profile_id());
Self::from_parts(
config.active_route_base_url(),
default_model,
provider_wire_format_for_config(&identity, Some(config)),
wire_format,
route_limits,
responses_profile_id,
false,
config,
)
.map(|mut client| {
@@ -1563,6 +1568,8 @@ impl CodewhaleClient {
config.default_model(),
provider_wire_format_for_config(&identity, Some(config)),
None,
None,
true,
config,
)
}
@@ -1600,6 +1607,8 @@ impl CodewhaleClient {
candidate.wire_model_id().as_str().to_string(),
candidate.protocol(),
crate::route_budget::known_route_limits(candidate.limits()),
candidate.responses_profile_id(),
false,
)
}
@@ -1616,6 +1625,8 @@ impl CodewhaleClient {
candidate.wire_model_id().as_str().to_string(),
candidate.protocol(),
crate::route_budget::known_route_limits(candidate.limits()),
candidate.responses_profile_id(),
false,
)
}
@@ -1765,6 +1776,8 @@ impl CodewhaleClient {
default_model: String,
wire_format: WireFormat,
route_limits: Option<RouteLimits>,
responses_profile_id: Option<ResponsesProfileId>,
catalog_probe: bool,
config: &Config,
) -> Result<Self> {
let admitted = Self::admit_client_route(config, base_url)?;
@@ -1778,14 +1791,15 @@ impl CodewhaleClient {
default_model,
wire_format,
route_limits,
responses_profile_id,
catalog_probe,
)
}
/// Shared constructor body for every admitted route.
///
/// `base_url` and `default_model` are the only inputs that differ between
/// the entry points; everything else (auth, provider, retry, headers,
/// timeouts) is derived from `config` so the paths cannot drift.
/// Auth, provider, retry, headers, and timeouts come from `config`.
/// The route, wire format, and Responses profile are the entry point's inputs.
fn from_admitted(
config: &Config,
admitted: AdmittedClientRoute,
@@ -1793,6 +1807,8 @@ impl CodewhaleClient {
default_model: String,
wire_format: WireFormat,
route_limits: Option<RouteLimits>,
responses_profile_id: Option<ResponsesProfileId>,
catalog_probe: bool,
) -> Result<Self> {
anyhow::ensure!(
admitted.identity == captured.identity && admitted.base_url == captured.base_url,
@@ -1803,6 +1819,12 @@ impl CodewhaleClient {
let root_cert = captured.root_cert;
let admitted_identity = admitted.identity;
let api_provider = admitted_identity.provider;
let responses_profile_id = admit_responses_profile(
api_provider,
wire_format,
responses_profile_id,
catalog_probe,
)?;
let context_window_override = config.context_window_for_provider_config(&admitted_identity);
let model_context_windows = config
.model_context_windows_for(&admitted_identity)
@@ -2028,6 +2050,7 @@ impl CodewhaleClient {
codex_account_id,
chatgpt_reasoning_api,
wire_format,
responses_profile_id,
retry,
isolated_request_state: false,
remote_control_inference_participant: !config.runtime_chat_isolated
@@ -2258,6 +2281,9 @@ impl CodewhaleClient {
base_url_override: Some(self.base_url.clone()),
limit_overrides: Vec::new(),
wire_override: (self.api_provider == ProviderKind::Custom).then_some(self.wire_format),
responses_profile_override: (self.api_provider == ProviderKind::Custom)
.then(|| self.responses_profile_id.map(|id| id.as_str().to_string()))
.flatten(),
};
if self.api_provider == ProviderKind::OpenaiCodex {
static CODEX_RESOLVER: OnceLock<RouteResolver> = OnceLock::new();
@@ -2365,6 +2391,7 @@ impl CodewhaleClient {
self.model_context_windows.as_ref(),
&self.configured_models,
(self.api_provider == ProviderKind::Custom).then_some(self.wire_format),
None,
)
.ok()
.filter(|route| {
@@ -2772,6 +2799,45 @@ fn is_auth_dialect_header(header_name: &HeaderName) -> bool {
|| header_name == HeaderName::from_static("x-api-key")
}
fn admit_responses_profile(
provider: ProviderKind,
wire_format: WireFormat,
profile: Option<ResponsesProfileId>,
catalog_probe: bool,
) -> Result<Option<ResponsesProfileId>> {
if catalog_probe {
anyhow::ensure!(
profile.is_none(),
"catalog refresh cannot carry a responses profile"
);
return Ok(None);
}
if wire_format == WireFormat::Responses {
let Some(profile) = profile else {
bail!(
"responses client for {} requires a dialect profile",
provider.as_str()
);
};
return Ok(Some(profile));
}
anyhow::ensure!(
profile.is_none(),
"client for {} cannot carry a responses profile on {wire_format:?}",
provider.as_str()
);
Ok(None)
}
#[cfg(test)]
fn standard_responses_profile() -> ResponsesProfileId {
codewhale_config::responses_dialect::builtin_profiles()
.iter()
.find(|profile| profile.id.as_str() == "standard")
.expect("standard responses profile")
.id
}
fn provider_default_wire_format(provider: ProviderKind) -> WireFormat {
provider
.provider()
@@ -3441,16 +3507,34 @@ impl CodewhaleClient {
.with_omitted_tool_names(omitted_tool_names))
}
WireFormat::Responses => {
let Some(profile_id) = self.responses_profile_id else {
bail!(
"responses client for {} has no dialect profile",
self.api_provider.as_str()
);
};
let Some(profile) =
codewhale_config::responses_dialect::profile(profile_id.as_str())
else {
bail!(
"responses profile {} is not in the dialect table",
profile_id.as_str()
);
};
let (mut body, omitted_tool_names) = responses::build_responses_wire_body(
&request,
self.api_provider,
self.chatgpt_reasoning_api.as_deref(),
profile,
responses::ResponsesReplayContext {
provider_id: self.api_provider.as_str(),
wire_model_id: request.model.as_str(),
reasoning_api_id: self.chatgpt_reasoning_api.as_deref(),
},
)?;
if let Some(model) = &declared_wire_model {
body["model"] = json!(model);
}
let is_codex = self.api_provider == ProviderKind::OpenaiCodex;
let url = responses_api_url(&self.base_url, self.api_provider);
let url = responses_api_url(&self.base_url, profile.endpoint.path);
let shape = if is_codex {
RouteShape::CodexResponses
} else if self.api_provider == ProviderKind::OpencodeZen {
@@ -3477,6 +3561,7 @@ impl CodewhaleClient {
.with_omitted_tool_names(omitted_tool_names))
}
}
.map(|prepared| prepared.with_responses_profile_id(self.responses_profile_id))
}
pub fn apply_provider_routing(&self, body: &mut Value) {
@@ -5300,6 +5385,9 @@ impl CodewhaleClient {
endpoint_fingerprint: prepared.endpoint_fingerprint(),
wire_model: prepared.wire_model.clone(),
wire_dialect: prepared.dialect.as_str().to_owned(),
responses_profile_id: prepared
.responses_profile_id
.map(|id| id.as_str().to_owned()),
tools: prepared.tool_wire_measurement(),
image_projection: prepared.image_projection.clone(),
});
@@ -7232,6 +7320,8 @@ mod configured_model_client_tests {
"initial-model".into(),
WireFormat::ChatCompletions,
None,
None,
false,
&config,
)
.unwrap();
@@ -7333,6 +7423,8 @@ mod configured_model_client_tests {
config.default_model(),
WireFormat::ChatCompletions,
None,
None,
false,
&config,
)
.unwrap();
+11
View File
@@ -27,6 +27,7 @@ use serde::Serialize;
use serde_json::Value;
use codewhale_config::provider::WireFormat;
use codewhale_config::responses_dialect::ResponsesProfileId;
use codewhale_runtime::hashing::canonical_json;
use codewhale_config::ProviderKind;
@@ -295,6 +296,9 @@ pub struct PreparedOutboundRequest {
/// Which transport entry point prepared this request. Never a substitute
/// for [`Self::wire_stream_field`], which is the wire truth.
pub entrypoint: CallerStreamMode,
/// Dialect id copied from the client. It is not inserted into `body`, and
/// `canonical_body` / `body_sha256` do not cover it.
pub responses_profile_id: Option<ResponsesProfileId>,
/// Wire-normalized tool names omitted by the route tool projection for
/// this request. It only transports a bounded user-visible warning.
pub omitted_tool_names: Vec<String>,
@@ -321,6 +325,7 @@ impl PreparedOutboundRequest {
reasoning,
replay_input_tokens,
entrypoint,
responses_profile_id: None,
omitted_tool_names: Vec::new(),
image_omission_notice: None,
image_projection: None,
@@ -339,6 +344,12 @@ impl PreparedOutboundRequest {
self
}
#[must_use]
pub fn with_responses_profile_id(mut self, profile: Option<ResponsesProfileId>) -> Self {
self.responses_profile_id = profile;
self
}
/// The `stream` field **as it appears on the finished body**, or `None`
/// when the body carries no such field at all.
///
@@ -212,11 +212,18 @@ impl ProviderNativeSearchClient {
};
let url = match self.inner.api_provider {
ProviderKind::Openai | ProviderKind::Xai | ProviderKind::ModelstudioTokenPlan => {
api_url(&self.inner.base_url, "responses")
}
ProviderKind::Deepseek => {
responses_api_url(&self.inner.base_url, self.inner.api_provider)
responses_api_url(
&self.inner.base_url,
codewhale_config::responses_dialect::ResponsesPathPolicy::VersionedResponses,
)
}
ProviderKind::Deepseek => responses_api_url(
&self.inner.base_url,
codewhale_config::responses_dialect::bound_responses_path(
ProviderKind::Deepseek,
&self.inner.base_url,
),
),
ProviderKind::Anthropic => anthropic_messages_url(&self.inner.base_url),
ProviderKind::XiaomiMimo => api_url(&self.inner.base_url, "chat/completions"),
ProviderKind::Zai => api_url(&self.inner.base_url, "web_search"),
+192 -91
View File
@@ -7,11 +7,19 @@
//! This is intentionally separate from the Chat Completions path
//! (`client/chat.rs`) to avoid protocol hacks.
use anyhow::{Context, Result};
use anyhow::{Context, Result, bail};
use serde_json::{Value, json};
use crate::llm_client::StreamEventBox;
use codewhale_config::ProviderKind;
use codewhale_config::responses_dialect::{
AudioUsageFieldPolicy, CacheUsageFieldPolicy, CompletionPolicy, DoneMarkerPolicy,
EncryptedReasoningReplayPolicy, IncludePolicy, IncompleteResponsePolicy, InstructionsPlacement,
MalformedEventPolicy, OutputTokenFieldPolicy, ParallelToolCallPolicy,
PlainReasoningReplayPolicy, ReasoningEffortMapping, ReasoningSummaryPolicy,
ReasoningUsageFieldPolicy, ResponsesDialectProfile, SamplingPolicy, StorePolicy,
SystemRoleReplayPolicy, ToolEnvelopePolicy,
};
use codewhale_models::tool_projection::{
RouteToolProjection, ToolSchemaProjection, description_with_constraint_note, forced_tool_name,
project_tool_schema,
@@ -33,12 +41,22 @@ use super::wire::{
use super::wire::{extract_sse_data_value, next_sse_line, push_sse_event_data};
use crate::llm_client::LlmError;
const CHATGPT_TOOL_NAMESPACE: &str = "codewhale";
pub(super) struct ResponsesReplayContext<'a> {
pub(super) provider_id: &'a str,
pub(super) wire_model_id: &'a str,
pub(super) reasoning_api_id: Option<&'a str>,
}
/// Build the Responses API request body from a `MessageRequest`.
#[cfg(test)]
pub(super) fn build_responses_body(request: &MessageRequest) -> Value {
build_responses_body_for_provider(request, ProviderKind::OpenaiCodex, None)
fn profile_for_provider(provider: ProviderKind) -> &'static ResponsesDialectProfile {
let profile_id = match provider {
ProviderKind::OpenaiCodex => "codex",
ProviderKind::Concentrate => "concentrate",
ProviderKind::Deepseek => "deepseek_stateless_plain_reasoning",
_ => "standard",
};
codewhale_config::responses_dialect::profile(profile_id)
.unwrap_or_else(|| panic!("responses profile {profile_id}"))
}
#[cfg(test)]
@@ -47,48 +65,64 @@ pub(super) fn build_responses_body_for_provider(
provider: ProviderKind,
reasoning_api: Option<&str>,
) -> Value {
build_responses_wire_body(request, provider, reasoning_api)
.expect("responses body builds")
.0
build_responses_wire_body(
request,
profile_for_provider(provider),
ResponsesReplayContext {
provider_id: provider.as_str(),
wire_model_id: request.model.as_str(),
reasoning_api_id: reasoning_api,
},
)
.expect("responses body builds")
.0
}
/// Build a provider-aware Responses API request body.
///
/// DeepSeek-V4-Flash-0731 implements the Responses wire shape but is stateless
/// and exposes plain reasoning text rather than OpenAI encrypted summaries.
/// Keep those exact-route differences here instead of leaking them into the
/// provider-neutral message model.
pub(super) fn build_responses_wire_body(
#[cfg(test)]
pub(super) fn responses_input_for_provider(
request: &MessageRequest,
provider: ProviderKind,
reasoning_api: Option<&str>,
) -> Vec<Value> {
convert_messages_to_responses_input(
request,
profile_for_provider(provider),
ResponsesReplayContext {
provider_id: provider.as_str(),
wire_model_id: request.model.as_str(),
reasoning_api_id: reasoning_api,
},
)
}
/// Build a Responses request body from the admitted dialect profile.
///
/// Output limits are already applied to `request.max_tokens`. A mismatched
/// encrypted reasoning state is dropped and is not rewritten as plain text.
pub(super) fn build_responses_wire_body(
request: &MessageRequest,
profile: &ResponsesDialectProfile,
context: ResponsesReplayContext<'_>,
) -> Result<(Value, Vec<String>)> {
let is_deepseek = matches!(provider, ProviderKind::Deepseek);
// Concentrate documents `model`, `input`, `stream`, `max_output_tokens`,
// `tools` / `tool_choice` / `parallel_tool_calls`, and `reasoning.effort`;
// `store`, `include`, `instructions`, and `reasoning.summary` are absent
// from its parameter reference, so this route sends only documented
// fields and carries the system prompt as a leading `system` message item
// (a documented input role) instead of `instructions`.
// https://concentrate.ai/docs/api-reference/endpoint/request-parameters
let is_concentrate = provider == ProviderKind::Concentrate;
let model = &request.model;
let mut body = json!({
"model": model,
"stream": true,
});
if !is_deepseek && !is_concentrate {
if profile.request.store == StorePolicy::ExplicitFalse {
body["store"] = json!(false);
}
// Every Responses route receives the same resolved request envelope as
// Chat and Messages. Omitting this field let auxiliary Responses calls
// escape the central route cap and made preview unable to prove the wire
// allowance. The official ChatGPT plan preview contract disallows
// max_output_tokens, so that route carries no client-side output cap.
if request.max_tokens > 0 && provider != ProviderKind::OpenaiCodex {
// allowance. Profiles that omit the field still keep that internal cap;
// they only leave it off the JSON.
if request.max_tokens > 0
&& profile.request.max_output_tokens == OutputTokenFieldPolicy::WhenPositive
{
body["max_output_tokens"] = json!(request.max_tokens);
}
if is_deepseek {
if profile.request.sampling == SamplingPolicy::TemperatureAndTopP {
if let Some(temperature) = request.temperature {
body["temperature"] = json!(temperature);
}
@@ -97,14 +131,12 @@ pub(super) fn build_responses_wire_body(
}
}
// Supply minimal instructions when the caller did not provide a system prompt.
let instructions = system_to_instructions(request.system.clone())
.filter(|text| !text.trim().is_empty())
.unwrap_or_else(|| "You are a helpful assistant.".to_string());
// Convert messages to Responses input items.
let mut input = convert_messages_to_responses_input(request, provider, reasoning_api);
if is_concentrate {
let mut input = convert_messages_to_responses_input(request, profile, context);
if profile.request.instructions == InstructionsPlacement::LeadingSystemInput {
input.insert(
0,
json!({
@@ -158,12 +190,19 @@ pub(super) fn build_responses_wire_body(
);
}
if !responses_tools.is_empty() {
body["tools"] = if provider == ProviderKind::OpenaiCodex {
json!([{ "type": "namespace", "name": CHATGPT_TOOL_NAMESPACE,
"description": "Codewhale tools running under the user's local permissions.",
"tools": responses_tools }])
} else {
json!(responses_tools)
body["tools"] = match profile.tools.envelope {
ToolEnvelopePolicy::NamespaceWrapped => {
let Some(namespace) = profile.tools.namespace else {
anyhow::bail!(
"responses profile {} wraps tools without a namespace",
profile.id.as_str()
);
};
json!([{ "type": "namespace", "name": namespace,
"description": "Codewhale tools running under the user's local permissions.",
"tools": responses_tools }])
}
ToolEnvelopePolicy::FunctionArray => json!(responses_tools),
};
let tool_choice = request.tool_choice.as_ref().and_then(Value::as_str);
body["tool_choice"] = if tool_choice == Some("none") {
@@ -172,34 +211,30 @@ pub(super) fn build_responses_wire_body(
json!("auto")
};
// The plan preview decoder tracks one active function-call block.
body["parallel_tool_calls"] = json!(provider != ProviderKind::OpenaiCodex);
body["parallel_tool_calls"] =
json!(profile.tools.parallel_calls == ParallelToolCallPolicy::Allow);
}
}
// Preserve the selected Codex tier through the final wire boundary. The
// roster owns each model's available levels; this pure builder must not
// collapse newer tiers to an older model's xhigh ceiling. Other Responses
// providers retain their own compatibility vocabulary.
// The roster owns each model's available levels. This builder only chooses
// which existing effort function the profile names.
if let Some(raw) = request.reasoning_effort.as_deref()
&& let Some(effort) = if provider == ProviderKind::OpenaiCodex {
codex_responses_reasoning_effort(raw)
} else {
responses_reasoning_effort(raw, is_deepseek)
&& let Some(effort) = match profile.reasoning.effort_mapping {
ReasoningEffortMapping::Codex => codex_responses_reasoning_effort(raw),
ReasoningEffortMapping::Deepseek => responses_reasoning_effort(raw, true),
ReasoningEffortMapping::OpenaiCompatible => compatible_responses_reasoning_effort(raw),
}
{
body["reasoning"] = if is_deepseek || is_concentrate {
json!({ "effort": effort })
} else {
json!({
body["reasoning"] = match profile.reasoning.summary {
ReasoningSummaryPolicy::Omit => json!({ "effort": effort }),
ReasoningSummaryPolicy::Auto => json!({
"effort": effort,
"summary": "auto",
})
}),
};
}
// OpenAI Codex can replay encrypted reasoning. DeepSeek exposes plain
// `reasoning_text` and does not support `include`.
if !is_deepseek && !is_concentrate {
if profile.request.include == IncludePolicy::EncryptedReasoning {
body["include"] = json!(["reasoning.encrypted_content"]);
}
@@ -215,7 +250,29 @@ impl CodewhaleClient {
// Body, endpoint, and route shape all come from the shared
// prepared-request seam (`prepare_outbound_request`).
let body = &prepared.body;
let is_chatgpt = self.api_provider == ProviderKind::OpenaiCodex;
let Some(profile_id) = self.responses_profile_id else {
bail!(
"responses client for {} has no dialect profile",
self.api_provider.as_str()
);
};
let Some(profile) = codewhale_config::responses_dialect::profile(profile_id.as_str())
else {
bail!(
"responses profile {} is not in the dialect table",
profile_id.as_str()
);
};
admit_shared_responses_usage(profile);
let malformed_json = profile.stream.malformed_json;
let done_marker = profile.stream.done_marker;
let incomplete = profile.stream.incomplete;
let tool_namespace = profile.tools.namespace;
let strict_terminal = matches!(
profile.stream.completion,
CompletionPolicy::RequireSuccessfulResponseCompleted
);
let codex_identity = self.api_provider == ProviderKind::OpenaiCodex;
let url = prepared.endpoint.url.clone();
// The synthetic MessageStart below is emitted from inside the stream
// closure, which outlives `prepared`. Clone the wire model — the id
@@ -223,10 +280,12 @@ impl CodewhaleClient {
// remapping — rather than borrowing the request that no longer exists
// at this layer.
let wire_model = prepared.wire_model.clone();
let reasoning_api = if is_chatgpt {
self.chatgpt_reasoning_api.as_deref()
} else {
Some("openai-responses")
let reasoning_api = match profile.replay.encrypted_state {
EncryptedReasoningReplayPolicy::AccountApiModelScoped => {
self.chatgpt_reasoning_api.as_deref()
}
EncryptedReasoningReplayPolicy::ProviderApiModelScoped => Some("openai-responses"),
EncryptedReasoningReplayPolicy::Disabled => None,
};
let reasoning_origin = reasoning_api.map(|api| {
(
@@ -359,7 +418,7 @@ impl CodewhaleClient {
} else if let Some(value) = extract_sse_data_value(&line)
&& let Err(err) = push_sse_event_data(&mut event_data, value)
{
if is_chatgpt {
if strict_terminal {
yield Err(LlmError::ParseError(err.to_string()).into());
return;
}
@@ -374,7 +433,7 @@ impl CodewhaleClient {
}
Ok(None) => break,
Err(err) => {
if is_chatgpt {
if strict_terminal {
yield Err(LlmError::ParseError(err.to_string()).into());
return;
}
@@ -389,14 +448,16 @@ impl CodewhaleClient {
{
let data = data.as_str();
if data == "[DONE]" {
if !is_chatgpt { done = true; }
if done_marker == DoneMarkerPolicy::Terminal {
done = true;
}
break;
}
let event: Value = match serde_json::from_str(data) {
Ok(v) => v,
Err(e) => {
if is_chatgpt {
if malformed_json == MalformedEventPolicy::Fail {
yield Err(LlmError::ParseError("Invalid ChatGPT response event".into()).into());
return;
}
@@ -431,7 +492,10 @@ impl CodewhaleClient {
Some(content_block_counter - 1);
}
"function_call" => {
if is_chatgpt && item.get("namespace").and_then(Value::as_str) != Some(CHATGPT_TOOL_NAMESPACE) {
if let Some(namespace) = tool_namespace
&& item.get("namespace").and_then(Value::as_str)
!= Some(namespace)
{
yield Err(LlmError::ParseError("ChatGPT returned a function outside the Codewhale tool namespace".into()).into());
return;
}
@@ -603,15 +667,17 @@ impl CodewhaleClient {
}
}
"response.completed" | "response.incomplete" => {
if is_chatgpt && !event.get("response").is_some_and(Value::is_object) {
if strict_terminal && !event.get("response").is_some_and(Value::is_object) {
yield Err(LlmError::ParseError("ChatGPT completion event has no response".into()).into());
return;
}
if is_chatgpt && event_type == "response.completed" && string_at(&event, "/response/status") != Some("completed") {
if strict_terminal && event_type == "response.completed" && string_at(&event, "/response/status") != Some("completed") {
yield Err(LlmError::ParseError("ChatGPT completion event has an unsuccessful status".into()).into());
return;
}
if is_chatgpt && event_type == "response.incomplete" {
if event_type == "response.incomplete"
&& incomplete == IncompleteResponsePolicy::Fail
{
if let Some(usage_val) = event.pointer("/response/usage") {
yield Ok(StreamEvent::MessageDelta {
delta: MessageDelta { stop_reason: None, stop_sequence: None },
@@ -645,21 +711,24 @@ impl CodewhaleClient {
}
"error" | "response.failed" => {
let (code, msg) = responses_event_error_details(&event);
if is_chatgpt {
if strict_terminal {
if let Some(usage_val) = event.pointer("/response/usage") {
yield Ok(StreamEvent::MessageDelta {
delta: MessageDelta { stop_reason: None, stop_sequence: None },
usage: Some(parse_responses_usage(usage_val)),
});
}
if let Some(error) = LlmError::from_subscription_sharing_error_code(&code) {
if codex_identity
&& let Some(error) =
LlmError::from_subscription_sharing_error_code(&code)
{
yield Err(error.into());
return;
}
}
let code = super::redact_model_bound_text(&code, &error_secrets);
let msg = super::redact_model_bound_text(&msg, &error_secrets);
if is_chatgpt {
if strict_terminal {
yield Err(LlmError::ModelError(format!("Responses API error [{code}]: {msg}")).into());
return;
}
@@ -680,12 +749,12 @@ impl CodewhaleClient {
}
}
// ChatGPT requires a successful response.completed event; other
// Responses routes also accept their documented terminal markers.
// A bare HTTP EOF is truncation, and
// a MessageStop here would hand the turn a partial answer as done.
// A strict completion profile requires a successful response.completed
// event. Other Responses profiles also accept their documented
// terminal markers. A bare HTTP EOF is truncation, and a
// MessageStop here would hand the turn a partial answer as done.
if !done {
if is_chatgpt {
if strict_terminal {
yield Err(LlmError::ParseError("ChatGPT Responses stream closed before a successful completion event".into()).into());
return;
}
@@ -860,10 +929,11 @@ pub(super) fn responses_tool_output(content: &str, content_blocks: Option<&[Valu
/// Convert Codewhale messages to Responses API input items.
pub(super) fn convert_messages_to_responses_input(
request: &MessageRequest,
provider: ProviderKind,
reasoning_api: Option<&str>,
profile: &ResponsesDialectProfile,
context: ResponsesReplayContext<'_>,
) -> Vec<Value> {
let is_deepseek = matches!(provider, ProviderKind::Deepseek);
let plain_with_placeholder =
profile.replay.plain_reasoning == PlainReasoningReplayPolicy::PlainTextWithToolPlaceholder;
let mut items = Vec::new();
for msg in &request.messages {
@@ -928,13 +998,18 @@ pub(super) fn convert_messages_to_responses_input(
return None;
};
if let Some(state) = state {
let matches = state.provider == provider.as_str()
&& if provider == ProviderKind::OpenaiCodex {
reasoning_api.is_some_and(|api| state.api == api)
} else {
let api_matches = match profile.replay.encrypted_state {
EncryptedReasoningReplayPolicy::AccountApiModelScoped => {
context.reasoning_api_id.is_some_and(|api| state.api == api)
}
EncryptedReasoningReplayPolicy::ProviderApiModelScoped => {
state.api == "openai-responses"
}
&& state.model == request.model;
EncryptedReasoningReplayPolicy::Disabled => false,
};
let matches = state.provider == context.provider_id
&& api_matches
&& state.model == context.wire_model_id;
if !matches {
return None;
}
@@ -948,7 +1023,12 @@ pub(super) fn convert_messages_to_responses_input(
}
return Some(item);
}
is_deepseek.then(|| {
matches!(
profile.replay.plain_reasoning,
PlainReasoningReplayPolicy::PlainText
| PlainReasoningReplayPolicy::PlainTextWithToolPlaceholder
)
.then(|| {
let text = if thinking.trim().is_empty() {
REASONING_REPLAY_PLACEHOLDER
} else {
@@ -963,7 +1043,7 @@ pub(super) fn convert_messages_to_responses_input(
})
})
};
if is_deepseek {
if plain_with_placeholder {
let before = items.len();
items.extend(msg.content.iter().filter_map(&reasoning_item));
if items.len() == before
@@ -1012,12 +1092,12 @@ pub(super) fn convert_messages_to_responses_input(
"name": to_api_tool_name(name),
"arguments": serde_json::to_string(input).unwrap_or_default(),
});
if provider == ProviderKind::OpenaiCodex {
item["namespace"] = json!(CHATGPT_TOOL_NAMESPACE);
if let Some(namespace) = profile.tools.namespace {
item["namespace"] = json!(namespace);
}
items.push(item);
}
ContentBlock::Thinking { .. } if !is_deepseek => {
ContentBlock::Thinking { .. } if !plain_with_placeholder => {
if let Some(item) = reasoning_item(block) {
items.push(item);
}
@@ -1069,7 +1149,14 @@ pub(super) fn convert_messages_to_responses_input(
if !content_items.is_empty() {
items.push(json!({
"type": "message",
"role": if provider == ProviderKind::OpenaiCodex && role == "system" { "developer" } else { role },
"role": if profile.replay.system_role
== SystemRoleReplayPolicy::SystemToDeveloper
&& role == "system"
{
"developer"
} else {
role
},
"content": content_items,
}));
}
@@ -1236,6 +1323,20 @@ pub(super) fn parse_tool_use_id(id: &str) -> (String, String) {
}
}
fn admit_shared_responses_usage(profile: &ResponsesDialectProfile) {
match (
profile.usage.cache_fields,
profile.usage.reasoning_fields,
profile.usage.audio_fields,
) {
(
CacheUsageFieldPolicy::TopLevelThenNestedFallback,
ReasoningUsageFieldPolicy::OutputTokensDetails,
AudioUsageFieldPolicy::NestedDetails,
) => {}
}
}
/// Parse usage from a Responses API usage object.
fn parse_responses_usage(val: &Value) -> Usage {
let input = val
+10 -8
View File
@@ -610,7 +610,7 @@ fn responses_body_serializes_the_child_catalog_without_duplication() {
let tools = crate::tools::subagent::kimi_general_child_request_tools_fixture();
let mut request = minimal_responses_request();
request.tools = Some(tools);
let body = build_responses_body(&request);
let body = build_responses_body_for_provider(&request, ProviderKind::OpenaiCodex, None);
assert_eq!(body["parallel_tool_calls"], false);
let generic = build_responses_body_for_provider(&request, ProviderKind::Openai, None);
assert_eq!(generic["parallel_tool_calls"], true);
@@ -1198,6 +1198,8 @@ async fn generic_responses_captures_and_replays_opaque_reasoning() {
"gpt-5.5".into(),
codewhale_config::provider::WireFormat::Responses,
None,
Some(crate::client::standard_responses_profile()),
false,
&config,
)
.unwrap();
@@ -1276,7 +1278,7 @@ fn codex_responses_body_uses_responses_reasoning_not_deepseek_thinking() {
top_p: None,
};
let body = build_responses_body(&request);
let body = build_responses_body_for_provider(&request, ProviderKind::OpenaiCodex, None);
assert_eq!(
body.pointer("/reasoning/effort").and_then(Value::as_str),
@@ -1529,7 +1531,7 @@ fn responses_input_includes_user_role_tool_results() {
top_p: None,
};
let input = convert_messages_to_responses_input(&request, ProviderKind::OpenaiCodex, None);
let input = responses_input_for_provider(&request, ProviderKind::OpenaiCodex, None);
assert_eq!(input[0]["type"], "function_call");
assert_eq!(input[0]["call_id"], "call_abc");
@@ -1567,12 +1569,12 @@ fn responses_input_encodes_tool_call_names() {
top_p: None,
};
let input = convert_messages_to_responses_input(&request, ProviderKind::OpenaiCodex, None);
let input = responses_input_for_provider(&request, ProviderKind::OpenaiCodex, None);
assert_eq!(input[0]["type"], "function_call");
assert_eq!(input[0]["name"], to_api_tool_name("web.run"));
assert_eq!(input[0]["namespace"], "codewhale");
let generic = convert_messages_to_responses_input(&request, ProviderKind::Openai, None);
let generic = responses_input_for_provider(&request, ProviderKind::Openai, None);
assert!(generic[0].get("namespace").is_none());
}
@@ -1697,7 +1699,7 @@ fn user_image_becomes_an_input_image_item() {
},
});
let items = convert_messages_to_responses_input(&request, ProviderKind::OpenaiCodex, None);
let items = responses_input_for_provider(&request, ProviderKind::OpenaiCodex, None);
let user = items
.iter()
@@ -1751,7 +1753,7 @@ fn tool_result_image_becomes_native_function_output_content() {
},
];
let items = convert_messages_to_responses_input(&request, ProviderKind::OpenaiCodex, None);
let items = responses_input_for_provider(&request, ProviderKind::OpenaiCodex, None);
let output = items
.iter()
.find(|item| item["type"] == "function_call_output")
@@ -1796,7 +1798,7 @@ fn responses_input_preserves_system_history_with_the_provider_role() {
(ProviderKind::OpenaiCodex, "developer"),
(ProviderKind::Openai, "system"),
] {
let items = convert_messages_to_responses_input(&request, provider, None);
let items = responses_input_for_provider(&request, provider, None);
let system = items
.iter()
.find(|item| item["role"] == role)
+1 -1
View File
@@ -374,7 +374,7 @@ mod adapter_agreement_tests {
#[test]
fn responses_preserves_positioned_instruction_history_and_marks_interrupted_history() {
let items = responses::convert_messages_to_responses_input(
let items = responses::responses_input_for_provider(
&request(transcript()),
ProviderKind::Openai,
None,
+1 -1
View File
@@ -4,7 +4,7 @@ use crate::client::chat::{
build_chat_messages_for_request_and_provider, count_reasoning_replay_chars, parse_chat_message,
parse_sse_chunk, sanitize_thinking_mode_messages,
};
use crate::client::responses::build_responses_body;
use crate::client::responses::build_responses_body_for_provider;
use crate::tools::apply_patch::ApplyPatchTool;
use crate::tools::spec::ToolSpec;
use crate::tools::{ToolContext, ToolRegistryBuilder};
+4 -1
View File
@@ -1292,7 +1292,10 @@
assert_eq!(client.wire_format, WireFormat::Responses);
assert_eq!(client.api_provider, ProviderKind::Concentrate);
assert_eq!(
responses_api_url(DEFAULT_CONCENTRATE_BASE_URL, ProviderKind::Concentrate),
responses_api_url(
DEFAULT_CONCENTRATE_BASE_URL,
codewhale_config::responses_dialect::ResponsesPathPolicy::VersionedResponses,
),
"https://api.concentrate.ai/v1/responses",
"the official base URL maps to the documented Responses endpoint"
);
+2 -1
View File
@@ -1005,7 +1005,8 @@ fn configured_secret_redaction_reaches_all_protocol_bodies() {
let chat = serde_json::to_string(&build_chat_messages_for_request(&prepared))
.expect("serialize Chat Completions body");
let anthropic = client.build_anthropic_body(&prepared, false).to_string();
let responses = build_responses_body(&prepared).to_string();
let responses =
build_responses_body_for_provider(&prepared, ProviderKind::OpenaiCodex, None).to_string();
for (route, body) in [
("chat", chat.as_str()),
@@ -898,11 +898,14 @@
Some("route-cap-test".to_string()),
Some("https://route-cap.example/v1".to_string()),
);
let profile = (wire_format == WireFormat::Responses).then(crate::client::standard_responses_profile);
CodewhaleClient::from_parts(
"https://route-cap.example/v1".to_string(),
"DeepSeek-V4-Flash".to_string(),
wire_format,
Some(limits),
profile,
false,
&config,
)
.expect("route cap test client")
@@ -970,6 +973,8 @@
model.to_string(),
wire,
Some(RouteLimits::default()),
None,
false,
&config,
)
.unwrap()
@@ -1379,6 +1384,8 @@
output_tokens: Some(128),
..RouteLimits::default()
}),
None,
false,
&config,
)
.expect("FIM route client");
+3
View File
@@ -714,6 +714,9 @@ impl Engine {
.as_deref()
.map(SafeLabel::identifier),
dialect: prepared.dialect.as_str().to_string(),
responses_profile_id: prepared
.responses_profile_id
.map(|id| id.as_str().to_string()),
route_shape: prepared.endpoint.shape.as_str().to_string(),
endpoint_host_class: prepared.safe_endpoint_host_class(),
endpoint_fingerprint: prepared.endpoint_fingerprint(),
+1
View File
@@ -553,6 +553,7 @@ fn offline_seed_route_for_a_vision_model_keeps_the_image() {
base_url_override: None,
limit_overrides: Vec::new(),
wire_override: None,
responses_profile_override: None,
})
.expect("bundled Anthropic route resolves offline");
let mut messages = vec![message_with_image("data:image/png;base64,QUJD")];
+3
View File
@@ -1532,6 +1532,9 @@ pub struct OutboundWireObservation {
pub endpoint_fingerprint: String,
pub wire_model: String,
pub wire_dialect: String,
/// Responses dialect id from the prepared request. Empty for other wires.
#[serde(skip_serializing_if = "Option::is_none")]
pub responses_profile_id: Option<String>,
pub tools: ToolWireMeasurement,
#[serde(skip_serializing_if = "Option::is_none")]
pub image_projection: Option<ImageProjectionReceipt>,
+1
View File
@@ -5078,6 +5078,7 @@ mod tests {
base_url_override: None,
limit_overrides: Vec::new(),
wire_override: None,
responses_profile_override: None,
})
};
+9
View File
@@ -495,6 +495,15 @@ pub fn route_is_valid_for_model(
)
})
.flatten(),
responses_profile_override: match codewhale_config::provider::responses_profile_override(
kind,
identity.key.as_str(),
config.provider_wire_dialect(identity),
config.provider_responses_profile(identity),
) {
Ok(profile) => profile,
Err(_) => return false,
},
};
RouteResolver::bundled()
.resolve(&request)
+5
View File
@@ -374,6 +374,10 @@ pub struct RouteFacts {
/// Named custom-provider / route identity from the resolved turn plan.
pub route_id: Option<SafeLabel>,
pub dialect: String,
/// Responses dialect id taken from the prepared request. Empty for every
/// other wire. The id is not part of the request body.
#[serde(skip_serializing_if = "Option::is_none")]
pub responses_profile_id: Option<String>,
pub route_shape: String,
/// Safe endpoint class. Remote authorities are represented only by a
/// bounded digest; even a credential-shaped tenant subdomain is never
@@ -1182,6 +1186,7 @@ pub mod test_support {
provider_display: SafeLabel::identifier("Z.ai"),
route_id: Some(SafeLabel::identifier("my-gateway")),
dialect: "chat-completions".to_string(),
responses_profile_id: None,
route_shape: "standard".to_string(),
endpoint_host_class: "https remote sha256:0123456789ab".to_string(),
endpoint_fingerprint: codewhale_runtime::hashing::sha256_hex(b"endpoint"),
+16
View File
@@ -646,6 +646,7 @@ pub fn resolve_declared_model_candidate(
model_context_windows,
models,
None,
None,
)
}
@@ -687,6 +688,7 @@ pub fn resolve_declared_model_candidate_with_wire_override(
model_context_windows: Option<&BTreeMap<String, u32>>,
models: &[codewhale_config::catalog::configured::ConfiguredModel],
wire_override: Option<codewhale_config::provider::WireFormat>,
responses_profile_override: Option<String>,
) -> Result<RouteCandidateResolution, String> {
let (resolver, endpoint_catalog_authoritative, resolver_reads_models_dev) =
live_route_resolver(provider, Some(identity), base_url);
@@ -704,6 +706,7 @@ pub fn resolve_declared_model_candidate_with_wire_override(
endpoint_catalog_authoritative,
resolver_reads_models_dev,
wire_override,
responses_profile_override,
)
}
@@ -725,6 +728,7 @@ pub fn resolve_route_candidate_with_context_metadata(
model_context_windows,
provider_reported_context,
None,
None,
)
}
@@ -739,6 +743,7 @@ pub fn resolve_route_candidate_with_context_metadata_and_wire_override(
model_context_windows: Option<&BTreeMap<String, u32>>,
provider_reported_context: Option<ProviderReportedKimiCodeContext>,
wire_override: Option<codewhale_config::provider::WireFormat>,
responses_profile_override: Option<String>,
) -> Result<RouteCandidateResolution, String> {
let resolver_base_url = base_url_override
.clone()
@@ -758,6 +763,7 @@ pub fn resolve_route_candidate_with_context_metadata_and_wire_override(
endpoint_catalog_authoritative,
resolver_reads_models_dev,
wire_override,
responses_profile_override,
)
}
@@ -798,6 +804,7 @@ fn resolve_route_candidate_with_catalog_resolver(
endpoint_catalog_authoritative: bool,
resolver_reads_models_dev: bool,
wire_override: Option<codewhale_config::provider::WireFormat>,
responses_profile_override: Option<String>,
) -> Result<RouteCandidateResolution, String> {
let effective_base_url = base_url_override
.as_deref()
@@ -813,6 +820,7 @@ fn resolve_route_candidate_with_catalog_resolver(
base_url_override,
limit_overrides: Vec::new(),
wire_override,
responses_profile_override,
};
// First pass: resolve the route without overrides to learn the effective
// endpoint, wire model id, and catalog limits. Candidates are immutable, so
@@ -1130,6 +1138,12 @@ pub fn resolve_runtime_route_for_identity(
)
})
.flatten();
let responses_profile_override = codewhale_config::provider::responses_profile_override(
provider,
identity.key.as_str(),
route_config.provider_wire_dialect(&identity),
route_config.provider_responses_profile(&identity),
)?;
// Every refreshed provider shares the same exact identity/endpoint gate.
// Codex keeps its separate authenticated account roster and protocol seam.
let resolution = if provider != ProviderKind::OpenaiCodex {
@@ -1187,6 +1201,7 @@ pub fn resolve_runtime_route_for_identity(
catalog.endpoint_catalog_authoritative,
true,
wire_override,
responses_profile_override.clone(),
)?
} else {
resolve_route_candidate_with_catalog_resolver(
@@ -1202,6 +1217,7 @@ pub fn resolve_runtime_route_for_identity(
false,
false,
wire_override,
responses_profile_override,
)?
};
let candidate = resolution.candidate;
+1
View File
@@ -367,6 +367,7 @@ fn model_preference(entry: &ProviderConfig) -> Option<&str> {
kind: None,
plugin_authority: None,
api_key_env: None,
responses_profile: None,
} if !model.trim().is_empty() && !model.chars().any(char::is_control) => Some(model),
_ => None,
}
+12
View File
@@ -2775,6 +2775,9 @@ pub struct ProviderConfig {
alias = "dialect"
)]
pub wire: Option<String>,
/// Responses dialect for a custom table whose `wire` is `responses`.
#[serde(default, alias = "responsesProfile")]
pub responses_profile: Option<String>,
#[serde(alias = "authMode")]
pub auth_mode: Option<String>,
/// Core-owned public-client OAuth descriptor for a named plugin provider.
@@ -4937,6 +4940,14 @@ impl Config {
.filter(|value| !value.is_empty())
}
pub fn provider_responses_profile(&self, identity: &ProviderIdentity) -> Option<&str> {
self.provider_config_for(identity)?
.responses_profile
.as_deref()
.map(str::trim)
.filter(|value| !value.is_empty())
}
pub fn verify_provider_identity(
&self,
identity: &ProviderIdentity,
@@ -10344,6 +10355,7 @@ fn merge_provider_config(base: ProviderConfig, override_cfg: ProviderConfig) ->
.or(base.model_context_windows),
mode: override_cfg.mode.or(base.mode),
wire: override_cfg.wire.or(base.wire),
responses_profile: override_cfg.responses_profile.or(base.responses_profile),
auth_mode: override_cfg.auth_mode.or(base.auth_mode),
oauth: override_cfg.oauth.or(base.oauth),
plugin_authority: base.plugin_authority,
+22
View File
@@ -12153,3 +12153,25 @@ fn legacy_root_model_preparation_remints_only_after_verified_model_selection() {
Some("custom")
);
}
#[test]
fn custom_provider_responses_profile_round_trips() {
let config: Config = toml::from_str(
r#"
[providers.my-gateway]
kind = "openai-compatible"
wire = "responses"
responses_profile = "standard"
"#,
)
.expect("custom responses profile parses");
let entry = config
.providers
.as_ref()
.expect("providers")
.custom_provider_config("my-gateway")
.expect("named table");
assert_eq!(entry.responses_profile.as_deref(), Some("standard"));
let rendered = toml::to_string(entry).expect("serialize");
assert!(rendered.contains("responses_profile = \"standard\""));
}
+4 -1
View File
@@ -14,6 +14,7 @@
//! | family | production path under test | proves |
//! |---|---|---|
//! | `sse` | `CodewhaleClient::create_message_stream` over a loopback HTTP server | wire bytes → normalized stream events |
//! | `responses` | route resolver, `prepare_outbound_request`, and the same loopback stream | profile id, request body, and Responses termination |
//! | `events` | `Engine::run` → `run_turn` with a scripted provider | one turn → protocol `EventMsg` sequence |
//! | `mcp` | `Engine::execute_mcp_tool_with_pool` + `McpPool::to_api_tools` against a transcript server | catalog and call results for one dispatch |
//! | `prompt` | the first model request of a real turn | model-visible prefix bytes (system prompt + tool catalog) |
@@ -30,7 +31,7 @@
//! no real provider, MCP server binary, or Node host. The `events`, `prompt`
//! and `hooks` goldens were recorded on Unix and are compiled only there:
//! hook fixtures are POSIX shell, and a Windows turn differs in shell and
//! path facts that no recorded golden covers yet. `sse` and `mcp` run
//! path facts that no recorded golden covers yet. `sse`, `responses`, and `mcp` run
//! everywhere.
#![cfg(test)]
@@ -48,6 +49,8 @@ mod mcp;
#[cfg(test)]
mod prompt;
#[cfg(test)]
mod responses;
#[cfg(test)]
mod sse;
#[cfg(test)]
mod stream_json;
+379
View File
@@ -0,0 +1,379 @@
//! Responses family: profile selection, request body, and stream termination.
//!
//! Route cases call `RouteResolver`. Request cases call
//! `prepare_outbound_request`. Stream cases replay `.sse` bytes through the
//! shared loopback into `create_message_stream`. The runner does not build a
//! second Responses parser.
use std::time::Duration;
use futures_util::StreamExt;
use serde_json::{Value, json};
use codewhale_config::ProviderKind;
use codewhale_config::provider::WireFormat;
use codewhale_config::route::{LogicalModelRef, RouteRequest, RouteResolver};
use codewhale_models::{ContentBlock, Message, MessageRequest, Role, SystemPrompt, Tool};
use codewhale_runtime::config::{Config, ProviderConfig, ProvidersConfig};
use super::golden::{self, Failures};
use super::sse::{Framing, LoopbackRecording};
use super::stream_json;
use crate::test_support::{EnvVarGuard, lock_test_env};
use codewhale_engine::client::CodewhaleClient;
use codewhale_engine::llm_client::LlmClient;
const FAMILY: &str = "responses";
const STREAM_DEADLINE: Duration = Duration::from_secs(20);
#[test]
fn responses_routes_match_goldens() {
let dir = golden::family_dir(FAMILY);
let names = golden::case_names(FAMILY)
.into_iter()
.filter(|name| golden::read_case(FAMILY, name)["kind"] == "route")
.collect::<Vec<_>>();
assert!(!names.is_empty(), "responses family has no route cases");
let mut failures = Failures::default();
for name in &names {
let case = golden::read_case(FAMILY, name);
let candidate = resolve_route(&case);
let profile_id = candidate
.responses_profile_id()
.map(|id| id.as_str().to_string());
if matches!(profile_id.as_deref(), Some("codex") | Some("concentrate")) {
assert_ne!(
candidate.endpoint().endpoint_key,
"responses",
"{name} must not depend on endpoint_key == responses"
);
}
let actual = json!({
"protocol": protocol_name(candidate.protocol()),
"profile_id": profile_id,
"endpoint_path_policy": profile_id.as_deref().map(path_policy_name),
});
failures.record(
name,
golden::check_golden(
&dir.join(format!("{name}.golden.json")),
&golden::pretty(&actual),
),
);
}
failures.finish(FAMILY, names.len());
}
#[test]
fn responses_requests_match_goldens() {
let _lock = lock_test_env();
let _token = EnvVarGuard::set("OPENAI_CODEX_ACCESS_TOKEN", "conformance-token");
let _legacy = EnvVarGuard::remove("CODEX_ACCESS_TOKEN");
let dir = golden::family_dir(FAMILY);
let names = golden::case_names(FAMILY)
.into_iter()
.filter(|name| golden::read_case(FAMILY, name)["kind"] == "request")
.collect::<Vec<_>>();
assert!(!names.is_empty(), "responses family has no request cases");
let mut failures = Failures::default();
for name in &names {
let case = golden::read_case(FAMILY, name);
let actual = match prepare_case(&case, None) {
Ok(actual) => actual,
Err(error) => {
failures.push(name, error);
continue;
}
};
failures.record(
name,
golden::check_golden(
&dir.join(format!("{name}.golden.json")),
&golden::pretty(&actual),
),
);
}
failures.finish(FAMILY, names.len());
}
#[tokio::test]
async fn responses_streams_match_goldens() {
let dir = golden::family_dir(FAMILY);
let names = golden::case_names(FAMILY)
.into_iter()
.filter(|name| golden::read_case(FAMILY, name)["kind"] == "stream")
.collect::<Vec<_>>();
assert!(!names.is_empty(), "responses family has no stream cases");
let mut failures = Failures::default();
for name in &names {
let case = golden::read_case(FAMILY, name);
let recording = std::fs::read(dir.join(format!("{name}.sse")))
.unwrap_or_else(|error| panic!("read {name}.sse: {error}"));
let mut lines = match replay_stream(&case, recording).await {
Ok(lines) => lines,
Err(error) => {
failures.push(name, error);
continue;
}
};
let mut masker = golden::Masker::new(&[]);
for line in &mut lines {
masker.value(line);
}
for line in lines.iter().skip(1) {
let kind = line["type"].as_str().unwrap_or_default();
if matches!(kind, "stream_failure" | "open_failure") {
continue;
}
if let Err(message) = stream_json::round_trips(line) {
failures.push(
name,
format!("normalized event is not StreamEvent: {message}"),
);
}
}
failures.record(
name,
golden::check_golden(
&dir.join(format!("{name}.golden.jsonl")),
&golden::jsonl(&lines),
),
);
}
failures.finish(FAMILY, names.len());
}
fn resolve_route(case: &Value) -> codewhale_config::route::ReadyRouteCandidate {
let provider = provider_kind(case);
RouteResolver::new()
.resolve(&RouteRequest {
explicit_provider: Some(provider),
model_selector: Some(LogicalModelRef::from(case["model"].as_str().unwrap())),
base_url_override: case["base_url"].as_str().map(str::to_string),
wire_override: (case["wire"].as_str() == Some("responses"))
.then_some(WireFormat::Responses),
..RouteRequest::default()
})
.unwrap_or_else(|error| panic!("resolve {}: {error}", case["model"]))
}
fn prepare_case(case: &Value, transport_base: Option<&str>) -> Result<Value, String> {
let _ = rustls::crypto::ring::default_provider().install_default();
let mut config = config_for(case, transport_base);
let _codex_grant =
if case["provider"].as_str() == Some("openai-codex") && transport_base.is_none() {
let home = tempfile::tempdir().map_err(|error| format!("codex home: {error}"))?;
let root = home
.path()
.canonicalize()
.map_err(|error| format!("codex home: {error}"))?;
let guard = EnvVarGuard::set("CODEWHALE_HOME", &root);
codewhale_runtime::oauth::install_test_chatgpt_registration(&mut config)
.map_err(|error| format!("codex grant: {error:#}"))?;
Some((home, guard))
} else {
None
};
let provider = provider_kind(case);
let resolved = codewhale_engine::route_runtime::resolve_runtime_route(
&config,
provider,
Some(case["model"].as_str().unwrap()),
)
.map_err(|error| format!("resolve: {error}"))?;
let profile_id = resolved
.candidate
.responses_profile_id()
.map(|id| id.as_str().to_string())
.ok_or_else(|| "request case resolved without a responses profile".to_string())?;
let client = CodewhaleClient::from_candidate(&resolved.config, &resolved.candidate)
.map_err(|error| format!("client: {error:#}"))?;
let prepared = client
.prepare_outbound_request(message_for_case(case), true)
.map_err(|error| format!("prepare: {error:#}"))?;
if prepared.responses_profile_id.map(|id| id.as_str()) != Some(profile_id.as_str()) {
return Err(format!(
"prepared profile {:?} does not match route profile {profile_id}",
prepared.responses_profile_id.map(|id| id.as_str())
));
}
Ok(json!({
"profile_id": profile_id,
"method": "POST",
"path": url_path(&prepared.endpoint.url),
"wire_model": prepared.wire_model,
"body": prepared.body,
}))
}
async fn replay_stream(case: &Value, recording: Vec<u8>) -> Result<Vec<Value>, String> {
let server = LoopbackRecording::start(recording, Framing::closed()).await;
let _lock = lock_test_env();
let _token = EnvVarGuard::set("OPENAI_CODEX_ACCESS_TOKEN", "conformance-token");
let _legacy = EnvVarGuard::remove("CODEX_ACCESS_TOKEN");
let _ = rustls::crypto::ring::default_provider().install_default();
let base = format!("{}/v1", server.base);
let config = config_for(case, Some(&base));
let resolved = codewhale_engine::route_runtime::resolve_runtime_route(
&config,
provider_kind(case),
Some(case["model"].as_str().unwrap()),
)
.map_err(|error| format!("resolve: {error}"))?;
let profile_id = resolved
.candidate
.responses_profile_id()
.map(|id| id.as_str().to_string())
.ok_or_else(|| "stream case resolved without a responses profile".to_string())?;
let client = CodewhaleClient::from_candidate(&resolved.config, &resolved.candidate)
.map_err(|error| format!("client: {error:#}"))?;
let events = golden::complete_within("Responses SSE replay", STREAM_DEADLINE, async {
let mut events = Vec::new();
match client.create_message_stream(message_for_case(case)).await {
Err(error) => events.push(json!({
"type": "open_failure",
"detail": format!("{error:#}"),
})),
Ok(mut stream) => {
while let Some(item) = stream.next().await {
match item {
Ok(event) => events.push(stream_json::to_json(&event)),
Err(error) => events.push(json!({
"type": "stream_failure",
"detail": format!("{error:#}"),
})),
}
}
}
}
events
})
.await?;
if events.is_empty() {
return Err("Responses replay produced no event or provider error".to_string());
}
let requests = server.requests.lock().expect("request log").clone();
if requests.is_empty() {
return Err("Responses recording was not exercised: no loopback request".to_string());
}
let mut lines = vec![json!({
"profile_id": profile_id,
"request": requests
.iter()
.map(|request| json!({ "method": request.method, "path": request.path }))
.collect::<Vec<_>>(),
})];
lines.extend(events);
Ok(lines)
}
fn config_for(case: &Value, transport_base: Option<&str>) -> Config {
let provider = case["provider"].as_str().unwrap();
let model = case["model"].as_str().unwrap().to_string();
let base_url = transport_base
.map(str::to_string)
.or_else(|| case["base_url"].as_str().map(str::to_string));
let mut entry = ProviderConfig {
model: Some(model),
base_url,
wire: case["wire"].as_str().map(str::to_string),
..ProviderConfig::default()
};
if provider != "openai-codex" {
entry.api_key = Some("conformance-key".to_string());
}
if provider == "custom" {
entry.kind = Some("openai-compatible".to_string());
}
let mut providers = ProvidersConfig::default();
match provider {
"deepseek" => providers.deepseek = entry,
"openai-codex" => providers.openai_codex = entry,
"concentrate" => providers.concentrate = entry,
"custom" => {
providers.custom.insert("gateway".to_string(), entry);
}
other => panic!("unknown conformance provider {other}"),
}
let mut config = Config::default();
config.provider = Some(if provider == "custom" {
"gateway".to_string()
} else {
provider.to_string()
});
config.providers = Some(providers);
config
}
fn message_for_case(case: &Value) -> MessageRequest {
let mut tools = None;
if case["tool"].as_bool() == Some(true) {
tools = Some(vec![Tool {
tool_type: None,
name: "read_file".to_string(),
description: "Read a file.".to_string(),
input_schema: json!({
"type": "object",
"properties": {"path": {"type": "string"}},
"required": ["path"]
}),
allowed_callers: None,
defer_loading: None,
input_examples: None,
strict: None,
cache_control: None,
}]);
}
MessageRequest {
model: case["model"].as_str().unwrap().to_string(),
messages: vec![Message {
role: Role::User,
content: vec![ContentBlock::Text {
text: case["user"]
.as_str()
.unwrap_or("Inspect the project.")
.to_string(),
cache_control: None,
}],
}],
max_tokens: case["max_tokens"].as_u64().unwrap_or(64) as u32,
system: case["system"]
.as_str()
.map(|text| SystemPrompt::Text(text.to_string())),
tools,
tool_choice: None,
metadata: None,
thinking: None,
reasoning_effort: case["reasoning_effort"].as_str().map(str::to_string),
stream: Some(true),
temperature: case["temperature"].as_f64().map(|value| value as f32),
top_p: case["top_p"].as_f64().map(|value| value as f32),
}
}
fn provider_kind(case: &Value) -> ProviderKind {
let name = case["provider"].as_str().unwrap();
ProviderKind::parse(name).unwrap_or_else(|| panic!("unknown provider {name}"))
}
fn protocol_name(protocol: WireFormat) -> &'static str {
match protocol {
WireFormat::Responses => "responses",
WireFormat::ChatCompletions => "chat_completions",
WireFormat::AnthropicMessages => "anthropic_messages",
}
}
fn path_policy_name(profile_id: &str) -> &'static str {
match profile_id {
"deepseek_stateless_plain_reasoning" => "unversioned_responses",
_ => "versioned_responses",
}
}
fn url_path(url: &str) -> String {
let rest = url.split_once("://").map(|(_, rest)| rest).unwrap_or(url);
rest.find('/')
.map(|index| rest[index..].to_string())
.unwrap_or_else(|| "/".to_string())
}
+19 -8
View File
@@ -28,9 +28,9 @@ const FAMILY: &str = "sse";
const STREAM_DEADLINE: Duration = Duration::from_secs(20);
#[derive(Debug, Clone)]
struct ReceivedRequest {
method: String,
path: String,
pub(super) struct ReceivedRequest {
pub(super) method: String,
pub(super) path: String,
}
#[tokio::test]
@@ -59,9 +59,9 @@ async fn empty_sse_recording_cannot_be_qualified() {
/// provider that stops sending); `chunked` framing uses HTTP chunks, and
/// `transport_cut` drops the connection before the terminating chunk so the
/// client sees a transport error rather than a clean EOF.
struct LoopbackRecording {
base: String,
requests: Arc<Mutex<Vec<ReceivedRequest>>>,
pub(super) struct LoopbackRecording {
pub(super) base: String,
pub(super) requests: Arc<Mutex<Vec<ReceivedRequest>>>,
task: tokio::task::JoinHandle<()>,
}
@@ -72,15 +72,26 @@ impl Drop for LoopbackRecording {
}
#[derive(Clone)]
struct Framing {
pub(super) struct Framing {
chunked: bool,
transport_cut: bool,
chunk_bytes: usize,
hold_open: bool,
}
impl Framing {
pub(super) fn closed() -> Self {
Self {
chunked: false,
transport_cut: false,
chunk_bytes: 0,
hold_open: false,
}
}
}
impl LoopbackRecording {
async fn start(body: Vec<u8>, framing: Framing) -> Self {
pub(super) async fn start(body: Vec<u8>, framing: Framing) -> Self {
let listener = tokio::net::TcpListener::bind("127.0.0.1:0")
.await
.expect("bind loopback");
+38 -28
View File
@@ -889,34 +889,44 @@ impl ProviderDashboardRow {
)
})
.flatten();
let route = if let Some(model) = declared_default {
codewhale_engine::route_runtime::resolve_declared_model_candidate_with_wire_override(
provider,
&provider_id,
model,
&route_base,
config.context_window_for_provider_config(&identity),
config.model_context_windows_for(&identity),
config.custom_models.as_deref().unwrap_or_default(),
wire_override,
)
} else {
// Browsing a provider is a snapshot projection, not execution
// admission. Actual local requests still require an explicit tag
// or a fresh endpoint-owned roster in resolve_runtime_route.
codewhale_engine::route_runtime::resolve_route_candidate_with_context_metadata_and_wire_override(
provider,
configured_model
.as_deref()
.or_else(|| identity.compatibility().map(|row| row.default_model)),
None,
Some(route_base.clone()),
config.context_window_for_provider_config(&identity),
config.model_context_windows_for(&identity),
None,
wire_override,
)
};
let responses_profile_override = codewhale_config::provider::responses_profile_override(
provider,
identity.key.as_str(),
config.provider_wire_dialect(&identity),
config.provider_responses_profile(&identity),
);
let route = responses_profile_override.and_then(|responses_profile_override| {
if let Some(model) = declared_default {
codewhale_engine::route_runtime::resolve_declared_model_candidate_with_wire_override(
provider,
&provider_id,
model,
&route_base,
config.context_window_for_provider_config(&identity),
config.model_context_windows_for(&identity),
config.custom_models.as_deref().unwrap_or_default(),
wire_override,
responses_profile_override,
)
} else {
// Browsing a provider is a snapshot projection, not execution
// admission. Actual local requests still require an explicit tag
// or a fresh endpoint-owned roster in resolve_runtime_route.
codewhale_engine::route_runtime::resolve_route_candidate_with_context_metadata_and_wire_override(
provider,
configured_model
.as_deref()
.or_else(|| identity.compatibility().map(|row| row.default_model)),
None,
Some(route_base.clone()),
config.context_window_for_provider_config(&identity),
config.model_context_windows_for(&identity),
None,
wire_override,
responses_profile_override,
)
}
});
let (
base_url,
supported_protocols,
+2 -1
View File
@@ -26,7 +26,7 @@ are failures in both modes and cannot be recorded as goldens. Provider timeout
errors are legitimate observable outcomes; a harness timing out is missing
evidence.
Platforms: `sse` and `mcp` run everywhere. `events`, `prompt` and `hooks`
Platforms: `sse`, `responses`, and `mcp` run everywhere. `events`, `prompt` and `hooks`
goldens were recorded on Unix and compile only there — hook fixtures are POSIX
shell, and a Windows turn differs in shell and path facts no golden covers
yet. Recording Windows goldens is open work, not an implied capability.
@@ -57,6 +57,7 @@ more `<name>.golden.*` files (output, recorded).
| family | input | production path | proves |
|---|---|---|---|
| `sse/` | `<name>.sse` recorded bytes + route | `CodewhaleClient::create_message_stream` via a loopback HTTP server | wire adapter: bytes → normalized stream events, incl. tool-call deltas, reasoning, usage, mid-stream errors, truncation |
| `responses/` | route, request, or `.sse` bytes | `RouteResolver`, `prepare_outbound_request`, and the same loopback stream | profile id, Responses request body, and termination differences |
| `events/` | scripted provider + host driver | `Engine::run` → `Engine::run_turn` | one turn → protocol `EventMsg` sequence + provider request count + workspace side effects |
| `mcp/` | scripted MCP server transcript + host steps | `McpPool` behind `Engine::execute_mcp_tool_with_pool` | catalog normalization and call results for one dispatch |
| `prompt/` | session config + workspace files | first `MessageRequest` of a real turn | model-visible prefix bytes (system prompt + tool catalog) |
@@ -0,0 +1,10 @@
{
"kind": "request",
"provider": "openai-codex",
"model": "gpt-5.5",
"system": "You are a coding assistant.",
"user": "Inspect the project.",
"max_tokens": 64,
"reasoning_effort": "high",
"tool": true
}
@@ -0,0 +1,59 @@
{
"profile_id": "codex",
"method": "POST",
"path": "/v1/responses",
"wire_model": "gpt-5.5",
"body": {
"model": "gpt-5.5",
"stream": true,
"store": false,
"instructions": "You are a coding assistant.",
"input": [
{
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "Inspect the project."
}
]
}
],
"tools": [
{
"type": "namespace",
"name": "codewhale",
"description": "Codewhale tools running under the user's local permissions.",
"tools": [
{
"type": "function",
"name": "read_file",
"description": "Read a file.",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string"
}
},
"required": [
"path"
]
},
"strict": false
}
]
}
],
"tool_choice": "auto",
"parallel_tool_calls": false,
"reasoning": {
"effort": "high",
"summary": "auto"
},
"include": [
"reasoning.encrypted_content"
]
}
}
@@ -0,0 +1,11 @@
{
"kind": "request",
"provider": "concentrate",
"model": "gpt-5.6-sol",
"base_url": "https://api.concentrate.ai/v1",
"system": "You are a coding assistant.",
"user": "Inspect the project.",
"max_tokens": 64,
"reasoning_effort": "high",
"tool": true
}
@@ -0,0 +1,57 @@
{
"profile_id": "concentrate",
"method": "POST",
"path": "/v1/responses",
"wire_model": "gpt-5.6-sol",
"body": {
"model": "gpt-5.6-sol",
"stream": true,
"max_output_tokens": 64,
"input": [
{
"type": "message",
"role": "system",
"content": [
{
"type": "input_text",
"text": "You are a coding assistant."
}
]
},
{
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "Inspect the project."
}
]
}
],
"tools": [
{
"type": "function",
"name": "read_file",
"description": "Read a file.",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string"
}
},
"required": [
"path"
]
},
"strict": false
}
],
"tool_choice": "auto",
"parallel_tool_calls": true,
"reasoning": {
"effort": "high"
}
}
}
@@ -0,0 +1,13 @@
{
"kind": "request",
"provider": "deepseek",
"model": "deepseek-v4-flash",
"base_url": "https://api.deepseek.com/v1",
"system": "You are a coding assistant.",
"user": "Inspect the project.",
"max_tokens": 64,
"reasoning_effort": "high",
"temperature": 0.5,
"top_p": 0.25,
"tool": true
}
@@ -0,0 +1,50 @@
{
"profile_id": "deepseek_stateless_plain_reasoning",
"method": "POST",
"path": "/responses",
"wire_model": "deepseek-v4-flash",
"body": {
"model": "deepseek-v4-flash",
"stream": true,
"max_output_tokens": 64,
"temperature": 0.5,
"top_p": 0.25,
"instructions": "You are a coding assistant.",
"input": [
{
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "Inspect the project."
}
]
}
],
"tools": [
{
"type": "function",
"name": "read_file",
"description": "Read a file.",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string"
}
},
"required": [
"path"
]
},
"strict": false
}
],
"tool_choice": "auto",
"parallel_tool_calls": true,
"reasoning": {
"effort": "high"
}
}
}
@@ -0,0 +1,12 @@
{
"kind": "request",
"provider": "custom",
"model": "gpt-5.6",
"base_url": "https://gateway.example/v1",
"wire": "responses",
"system": "You are a coding assistant.",
"user": "Inspect the project.",
"max_tokens": 64,
"reasoning_effort": "high",
"tool": true
}
@@ -0,0 +1,53 @@
{
"profile_id": "standard",
"method": "POST",
"path": "/v1/responses",
"wire_model": "gpt-5.6",
"body": {
"model": "gpt-5.6",
"stream": true,
"store": false,
"max_output_tokens": 64,
"instructions": "You are a coding assistant.",
"input": [
{
"type": "message",
"role": "user",
"content": [
{
"type": "input_text",
"text": "Inspect the project."
}
]
}
],
"tools": [
{
"type": "function",
"name": "read_file",
"description": "Read a file.",
"parameters": {
"type": "object",
"properties": {
"path": {
"type": "string"
}
},
"required": [
"path"
]
},
"strict": false
}
],
"tool_choice": "auto",
"parallel_tool_calls": true,
"reasoning": {
"effort": "high",
"summary": "auto"
},
"include": [
"reasoning.encrypted_content"
]
}
}
@@ -0,0 +1,5 @@
{
"kind": "route",
"provider": "openai-codex",
"model": "gpt-5.5"
}
@@ -0,0 +1,5 @@
{
"protocol": "responses",
"profile_id": "codex",
"endpoint_path_policy": "versioned_responses"
}
@@ -0,0 +1,5 @@
{
"kind": "route",
"provider": "concentrate",
"model": "gpt-5.6-sol"
}
@@ -0,0 +1,5 @@
{
"protocol": "responses",
"profile_id": "concentrate",
"endpoint_path_policy": "versioned_responses"
}
@@ -0,0 +1,6 @@
{
"kind": "route",
"provider": "deepseek",
"model": "deepseek-v4-flash",
"base_url": "https://compatible.example/v1"
}
@@ -0,0 +1,5 @@
{
"protocol": "chat_completions",
"profile_id": null,
"endpoint_path_policy": null
}
@@ -0,0 +1,5 @@
{
"kind": "route",
"provider": "deepseek",
"model": "deepseek-v4-flash"
}
@@ -0,0 +1,5 @@
{
"protocol": "responses",
"profile_id": "deepseek_stateless_plain_reasoning",
"endpoint_path_policy": "unversioned_responses"
}
@@ -0,0 +1,7 @@
{
"kind": "route",
"provider": "custom",
"model": "gpt-5.6",
"base_url": "https://gateway.example/v1",
"wire": "responses"
}
@@ -0,0 +1,5 @@
{
"protocol": "responses",
"profile_id": "standard",
"endpoint_path_policy": "versioned_responses"
}
@@ -0,0 +1,7 @@
{
"kind": "stream",
"provider": "openai-codex",
"model": "gpt-5.5",
"system": "You are a coding assistant.",
"user": "Hello"
}
@@ -0,0 +1,7 @@
{"profile_id":"codex","request":[{"method":"POST","path":"/v1/responses"}]}
{"type":"message_start","message":{"id":"","type":"message","role":"assistant","content":[],"model":"gpt-5.5","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}}
{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}
{"type":"content_block_stop","index":0}
{"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":3,"output_tokens":1}}
{"type":"message_stop"}
@@ -0,0 +1,8 @@
data: {"type":"response.output_item.added","item":{"type":"message","id":"msg_1","status":"in_progress","role":"assistant","content":[]}}
data: {"type":"response.output_text.delta","delta":"Hello"}
data: {"type":"response.output_item.done","item":{"type":"message","id":"msg_1","status":"completed"}}
data: {"type":"response.completed","response":{"id":"resp_1","status":"completed","usage":{"input_tokens":3,"output_tokens":1}}}
@@ -0,0 +1,7 @@
{
"kind": "stream",
"provider": "openai-codex",
"model": "gpt-5.5",
"system": "You are a coding assistant.",
"user": "Hello"
}
@@ -0,0 +1,3 @@
{"profile_id":"codex","request":[{"method":"POST","path":"/v1/responses"}]}
{"type":"message_start","message":{"id":"","type":"message","role":"assistant","content":[],"model":"gpt-5.5","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}}
{"type":"stream_failure","detail":"Response parsing error: ChatGPT Responses stream closed before a successful completion event"}
@@ -0,0 +1,2 @@
data: [DONE]
@@ -0,0 +1,7 @@
{
"kind": "stream",
"provider": "openai-codex",
"model": "gpt-5.5",
"system": "You are a coding assistant.",
"user": "Hello"
}
@@ -0,0 +1,4 @@
{"profile_id":"codex","request":[{"method":"POST","path":"/v1/responses"}]}
{"type":"message_start","message":{"id":"","type":"message","role":"assistant","content":[],"model":"gpt-5.5","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}}
{"type":"message_delta","delta":{"stop_reason":null,"stop_sequence":null},"usage":{"input_tokens":3,"output_tokens":1}}
{"type":"stream_failure","detail":"Response parsing error: ChatGPT response incomplete [max_tokens]: response incomplete: max_tokens"}
@@ -0,0 +1,2 @@
data: {"type":"response.incomplete","response":{"id":"resp_1","status":"incomplete","incomplete_details":{"reason":"max_tokens"},"usage":{"input_tokens":3,"output_tokens":1}}}
@@ -0,0 +1,7 @@
{
"kind": "stream",
"provider": "openai-codex",
"model": "gpt-5.5",
"system": "You are a coding assistant.",
"user": "Hello"
}
@@ -0,0 +1,3 @@
{"profile_id":"codex","request":[{"method":"POST","path":"/v1/responses"}]}
{"type":"message_start","message":{"id":"","type":"message","role":"assistant","content":[],"model":"gpt-5.5","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}}
{"type":"stream_failure","detail":"Response parsing error: Invalid ChatGPT response event"}
@@ -0,0 +1,2 @@
data: {not-json}
@@ -0,0 +1,8 @@
{
"kind": "stream",
"provider": "custom",
"model": "gpt-5.6",
"wire": "responses",
"system": "You are a coding assistant.",
"user": "Hello"
}
@@ -0,0 +1,7 @@
{"profile_id":"standard","request":[{"method":"POST","path":"/v1/responses"}]}
{"type":"message_start","message":{"id":"","type":"message","role":"assistant","content":[],"model":"gpt-5.6","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}}
{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}
{"type":"content_block_stop","index":0}
{"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":3,"output_tokens":1}}
{"type":"message_stop"}
@@ -0,0 +1,12 @@
data: {not-json}
data: {"type":"response.output_item.added","item":{"type":"message","id":"msg_1","status":"in_progress","role":"assistant","content":[]}}
data: {"type":"response.output_text.delta","delta":"Hello"}
data: {"type":"response.output_item.done","item":{"type":"message","id":"msg_1","status":"completed"}}
data: {"type":"response.completed","response":{"id":"resp_1","status":"completed","usage":{"input_tokens":3,"output_tokens":1}}}
data: [DONE]
@@ -0,0 +1,8 @@
{
"kind": "stream",
"provider": "custom",
"model": "gpt-5.6",
"wire": "responses",
"system": "You are a coding assistant.",
"user": "Hello"
}
@@ -0,0 +1,7 @@
{"profile_id":"standard","request":[{"method":"POST","path":"/v1/responses"}]}
{"type":"message_start","message":{"id":"","type":"message","role":"assistant","content":[],"model":"gpt-5.6","stop_reason":null,"stop_sequence":null,"usage":{"input_tokens":0,"output_tokens":0}}}
{"type":"content_block_start","index":0,"content_block":{"type":"text","text":""}}
{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}
{"type":"content_block_stop","index":0}
{"type":"message_delta","delta":{"stop_reason":"end_turn","stop_sequence":null},"usage":{"input_tokens":3,"output_tokens":1}}
{"type":"message_stop"}
@@ -0,0 +1,10 @@
data: {"type":"response.output_item.added","item":{"type":"message","id":"msg_1","status":"in_progress","role":"assistant","content":[]}}
data: {"type":"response.output_text.delta","delta":"Hello"}
data: {"type":"response.output_item.done","item":{"type":"message","id":"msg_1","status":"completed"}}
data: {"type":"response.completed","response":{"id":"resp_1","status":"completed","usage":{"input_tokens":3,"output_tokens":1}}}
data: [DONE]
+3
View File
@@ -692,8 +692,11 @@ base_url = "https://gateway.example/v1"
api_key_env = "MY_GATEWAY_API_KEY"
model = "gpt-5.6"
wire = "responses"
responses_profile = "standard"
```
`responses_profile` is optional. Leaving it out uses the `standard` Responses dialect. The only profile a custom table may name is `standard`; `codex` and any unknown id are rejected, and the field is rejected unless `wire` selects Responses.
StepFun has a first-class provider entry, so keep Coding Plan credentials and
base URL scoped to `[providers.stepfun]`:
+10
View File
@@ -1042,3 +1042,13 @@ status = "experimental"
surfaces = { tui = "preview", web = "partial", api = "partial" }
owner = "crates/engine/src/llm_client/rate_limit.rs"
docs = "docs/rate-limit-retries.md"
[[feature]]
id = "custom-responses-profile"
name = "Custom Responses profile"
summary = "A custom provider with wire = responses can name an allowlisted responses_profile; omitted means standard."
status = "stable"
surfaces = { tui = "stable" }
since = "unreleased"
docs = "docs/CONFIGURATION.md"
owner = "crates/config/src/provider_toml.rs"