mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-09 04:30:20 +08:00
feat: enforce full format field coverage audit
This commit is contained in:
@@ -15,7 +15,7 @@ use crate::{
|
||||
apply_gemini_request_extensions, canonical_extension_object_mut,
|
||||
canonical_openai_reasoning_effort, extract_gemini_model_from_path,
|
||||
gemini_contents_to_canonical_messages, gemini_extensions, gemini_generation_config,
|
||||
gemini_generation_config_extra, gemini_google_search_grounding, gemini_openai_extra_body,
|
||||
gemini_generation_config_extra, gemini_google_search_grounding,
|
||||
gemini_response_format_to_canonical, gemini_system_to_canonical_instructions,
|
||||
gemini_thinking_to_canonical, gemini_tool_choice_to_canonical, gemini_tools_to_canonical,
|
||||
gemini_value_by_case, CanonicalContentBlock, CanonicalMessage, CanonicalRequest,
|
||||
@@ -173,10 +173,6 @@ pub fn from_raw(body_json: &Value, request_path: &str) -> Option<CanonicalReques
|
||||
canonical_extension_object_mut(&mut canonical.extensions, "gemini")
|
||||
.insert("raw_tool_config".to_string(), tool_config);
|
||||
}
|
||||
if let Some(extra_body) = gemini_openai_extra_body(request) {
|
||||
canonical_extension_object_mut(&mut canonical.extensions, "openai")
|
||||
.insert("extra_body".to_string(), extra_body);
|
||||
}
|
||||
if let Some(web_search_options) = web_search_options {
|
||||
canonical_extension_object_mut(&mut canonical.extensions, "openai")
|
||||
.insert("web_search_options".to_string(), web_search_options);
|
||||
|
||||
@@ -5,7 +5,7 @@ use serde_json::{json, Map, Value};
|
||||
use crate::{
|
||||
formats::context::FormatContext,
|
||||
formats::openai::shared::{
|
||||
map_thinking_budget_to_openai_reasoning_effort, OpenAiReasoningEffort,
|
||||
map_thinking_budget_to_openai_reasoning_effort, OpenAiResponsesReasoningEffort,
|
||||
},
|
||||
protocol::canonical::{
|
||||
canonical_response_format_to_openai, canonicalize_tool_arguments,
|
||||
@@ -660,7 +660,9 @@ fn reasoning_config_to_responses(thinking: &CanonicalThinkingConfig) -> Option<V
|
||||
fn openai_responses_reasoning_effort(effort: &str) -> Option<&'static str> {
|
||||
match effort.trim().to_ascii_lowercase().as_str() {
|
||||
"max" => Some("xhigh"),
|
||||
value => OpenAiReasoningEffort::parse(value).map(OpenAiReasoningEffort::as_str),
|
||||
value => {
|
||||
OpenAiResponsesReasoningEffort::parse(value).map(OpenAiResponsesReasoningEffort::as_str)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,40 +2,50 @@ use serde_json::{Map, Value};
|
||||
|
||||
use crate::formats::shared::model_directives::ReasoningEffort;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum OpenAiReasoningEffort {
|
||||
None,
|
||||
Minimal,
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
XHigh,
|
||||
macro_rules! define_openai_reasoning_effort {
|
||||
($name:ident) => {
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum $name {
|
||||
None,
|
||||
Minimal,
|
||||
Low,
|
||||
Medium,
|
||||
High,
|
||||
XHigh,
|
||||
}
|
||||
|
||||
impl $name {
|
||||
pub fn parse(value: &str) -> Option<Self> {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"none" => Some(Self::None),
|
||||
"minimal" => Some(Self::Minimal),
|
||||
"low" => Some(Self::Low),
|
||||
"medium" => Some(Self::Medium),
|
||||
"high" => Some(Self::High),
|
||||
"xhigh" => Some(Self::XHigh),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::None => "none",
|
||||
Self::Minimal => "minimal",
|
||||
Self::Low => "low",
|
||||
Self::Medium => "medium",
|
||||
Self::High => "high",
|
||||
Self::XHigh => "xhigh",
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl OpenAiReasoningEffort {
|
||||
pub fn parse(value: &str) -> Option<Self> {
|
||||
match value.trim().to_ascii_lowercase().as_str() {
|
||||
"none" => Some(Self::None),
|
||||
"minimal" => Some(Self::Minimal),
|
||||
"low" => Some(Self::Low),
|
||||
"medium" => Some(Self::Medium),
|
||||
"high" => Some(Self::High),
|
||||
"xhigh" => Some(Self::XHigh),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
define_openai_reasoning_effort!(OpenAiChatReasoningEffort);
|
||||
define_openai_reasoning_effort!(OpenAiResponsesReasoningEffort);
|
||||
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
Self::None => "none",
|
||||
Self::Minimal => "minimal",
|
||||
Self::Low => "low",
|
||||
Self::Medium => "medium",
|
||||
Self::High => "high",
|
||||
Self::XHigh => "xhigh",
|
||||
}
|
||||
}
|
||||
}
|
||||
#[deprecated(note = "use OpenAiChatReasoningEffort or OpenAiResponsesReasoningEffort")]
|
||||
pub type OpenAiReasoningEffort = OpenAiChatReasoningEffort;
|
||||
|
||||
pub fn parse_openai_stop_sequences(stop: Option<&Value>) -> Option<Vec<Value>> {
|
||||
match stop {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5198,65 +5198,6 @@ pub(crate) fn gemini_generation_config_extra(
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn gemini_openai_extra_body(request: &Map<String, Value>) -> Option<Value> {
|
||||
let mut extra_body = Map::new();
|
||||
if let Some(generation_config) = request
|
||||
.get("generationConfig")
|
||||
.or_else(|| request.get("generation_config"))
|
||||
.and_then(Value::as_object)
|
||||
{
|
||||
let mut google = Map::new();
|
||||
if let Some(thinking_config) =
|
||||
gemini_value_by_case(generation_config, "thinkingConfig", "thinking_config").cloned()
|
||||
{
|
||||
google.insert("thinking_config".to_string(), thinking_config);
|
||||
}
|
||||
if let Some(response_modalities) = gemini_value_by_case(
|
||||
generation_config,
|
||||
"responseModalities",
|
||||
"response_modalities",
|
||||
)
|
||||
.cloned()
|
||||
{
|
||||
google.insert("response_modalities".to_string(), response_modalities);
|
||||
}
|
||||
if !google.is_empty() {
|
||||
extra_body.insert("google".to_string(), Value::Object(google));
|
||||
}
|
||||
|
||||
let generation_config_extra = gemini_generation_config_extra(generation_config);
|
||||
if !generation_config_extra.is_empty() {
|
||||
extra_body.insert(
|
||||
"gemini".to_string(),
|
||||
json!({ "generation_config_extra": generation_config_extra }),
|
||||
);
|
||||
}
|
||||
}
|
||||
if let Some(safety_settings) = request
|
||||
.get("safetySettings")
|
||||
.or_else(|| request.get("safety_settings"))
|
||||
.cloned()
|
||||
{
|
||||
let gemini = extra_body
|
||||
.entry("gemini".to_string())
|
||||
.or_insert_with(|| Value::Object(Map::new()))
|
||||
.as_object_mut()?;
|
||||
gemini.insert("safety_settings".to_string(), safety_settings);
|
||||
}
|
||||
if let Some(cached_content) = request
|
||||
.get("cachedContent")
|
||||
.or_else(|| request.get("cached_content"))
|
||||
.cloned()
|
||||
{
|
||||
let gemini = extra_body
|
||||
.entry("gemini".to_string())
|
||||
.or_insert_with(|| Value::Object(Map::new()))
|
||||
.as_object_mut()?;
|
||||
gemini.insert("cached_content".to_string(), cached_content);
|
||||
}
|
||||
(!extra_body.is_empty()).then_some(Value::Object(extra_body))
|
||||
}
|
||||
|
||||
pub(crate) fn extract_gemini_model_from_path(path: &str) -> Option<String> {
|
||||
let marker = "/models/";
|
||||
let start = path.find(marker)? + marker.len();
|
||||
|
||||
@@ -9,9 +9,11 @@ Statuses:
|
||||
- `native`: emitted as a target-native field without semantic change.
|
||||
- `mapped`: converted through canonical/provider-specific mapping.
|
||||
- `extension-preserved`: preserved in same-format canonical roundtrip or target-approved extension namespace.
|
||||
- `unsupported`: rejected because the source field is outside the audited source schema or conversion surface.
|
||||
- `lossy-blocked`: conversion fails closed.
|
||||
- `invalid-enum`: conversion fails closed because a provider enum value is not valid for the target mapping.
|
||||
- `pending`: parser/emitter exists, but strict field-by-field audit is not complete.
|
||||
|
||||
Full schema field coverage is tracked in `docs/api/format-field-coverage-matrix.md`. That matrix is generated from `docs/api/provider-interface-definitions.md` and gives every documented OpenAI, Claude, and Gemini schema field a handling status. “Handled” means mapped, same-format/native preserved, extension-preserved, blocked with a structured error, or explicitly marked outside the canonical conversion surface.
|
||||
|
||||
## Implemented Boundary Changes
|
||||
|
||||
@@ -22,7 +24,8 @@ Statuses:
|
||||
| Same-format provider path | Bypasses canonical conversion and copies the parsed JSON object before transport edits. |
|
||||
| Cross-format same-format-provider path | Uses `convert_request_pure`, then applies model/body/stream edits in transport. |
|
||||
| Conversion errors | Added `UnsupportedField`, `InvalidEnumValue`, `LossyConversionBlocked`, and `InvalidTargetField`. |
|
||||
| Reporting | Added `ConversionReport` with field statuses. Current report is top-level request-field oriented; nested exhaustive reporting is pending. |
|
||||
| Reporting | Added `ConversionReport` with field statuses. Runtime reports remain conversion-operation oriented; exhaustive nested schema coverage is enforced by `format-field-coverage-matrix.md`. |
|
||||
| Source schema coverage | Cross-format request conversion rejects unknown source root fields before emit. Every documented schema field is covered by the field coverage matrix. |
|
||||
| Tool schema roundtrip | Claude `input_schema` and Gemini `functionDeclarations.parameters` preserve raw same-format schema through provider-specific extensions. |
|
||||
| Tool result ids | Chat `tool_call_id`, Responses `call_id`, Claude `tool_use_id`, and Gemini `functionResponse.id` are mapped through canonical tool IDs. |
|
||||
|
||||
@@ -62,7 +65,7 @@ Statuses:
|
||||
| `safety_identifier` | OpenAI extension | `safety_identifier` | extension-preserved |
|
||||
| `prompt_cache_key` | OpenAI extension | `prompt_cache_key` | extension-preserved |
|
||||
| `user` | legacy Chat user field | none | lossy-blocked |
|
||||
| unknown top-level fields | OpenAI extension | none unless target-approved | pending strict nested reporting |
|
||||
| unknown top-level fields | source schema guard | none | unsupported |
|
||||
|
||||
## OpenAI Responses -> OpenAI Chat
|
||||
|
||||
@@ -99,11 +102,11 @@ Statuses:
|
||||
| `conversation` | Responses-only | none | lossy-blocked |
|
||||
| `background` | Responses-only | none | lossy-blocked |
|
||||
| `max_tool_calls` | Responses-only | none | lossy-blocked |
|
||||
| unknown top-level fields | Responses extension | none unless Chat-approved | pending strict nested reporting |
|
||||
| unknown top-level fields | source schema guard | none | unsupported |
|
||||
|
||||
## Claude Messages <-> OpenAI
|
||||
## Claude Messages <-> OpenAI Chat / Responses
|
||||
|
||||
Current parser/emitter coverage exists, but strict audit is pending for the third batch.
|
||||
Claude to OpenAI Chat, Claude to OpenAI Responses, and the reverse directions are included in the field coverage matrix. Runtime strict guards cover request root fields, provider extension namespaces, thinking/cache/tool-result hazards, and target generation-field gaps. Fields without a lossless target equivalent fail closed instead of being dropped.
|
||||
|
||||
High-risk fields:
|
||||
|
||||
@@ -115,11 +118,12 @@ High-risk fields:
|
||||
| `tools[].input_schema` | OpenAI tool parameters | mapped; raw same-format schema preservation implemented |
|
||||
| `tool_choice.disable_parallel_tool_use` | OpenAI `parallel_tool_calls` | mapped, implemented |
|
||||
| `tool_result` multi-block content | OpenAI tool output/content | same-format preserved; cross-format to Chat/Responses is lossy-blocked |
|
||||
| `metadata` / container fields | OpenAI metadata or extension | field-by-field decision pending |
|
||||
| `metadata` | OpenAI metadata | mapped when the target has metadata |
|
||||
| `container`, `inference_geo`, `service_tier` | OpenAI target has no audited equivalent | lossy-blocked unless a target-approved mapping is added |
|
||||
|
||||
## Gemini GenerateContent <-> OpenAI/Claude
|
||||
## Gemini GenerateContent <-> OpenAI Chat / Responses / Claude
|
||||
|
||||
Current parser/emitter coverage exists, but strict audit is pending for the fourth batch.
|
||||
Gemini to OpenAI Chat, Gemini to OpenAI Responses, Gemini to Claude, and reverse generation paths are included in the field coverage matrix. Gemini-only request fields are preserved same-format and blocked cross-format unless the target mapping is explicitly audited.
|
||||
|
||||
High-risk fields:
|
||||
|
||||
@@ -138,8 +142,7 @@ High-risk fields:
|
||||
|
||||
## Embedding And Rerank
|
||||
|
||||
Fifth batch first pass is implemented for request parse/emit capability and strict target
|
||||
guards. Nested per-field reporting is still pending.
|
||||
Embedding and rerank request parse/emit capability and strict target guards are implemented. Provider schema fields outside these canonical conversion surfaces are marked `not-in-conversion-surface` in the field coverage matrix instead of being left implicit.
|
||||
|
||||
Embedding source capability:
|
||||
|
||||
@@ -199,8 +202,8 @@ still outside canonical conversion.
|
||||
Sixth batch first pass is implemented for unknown event handling and runtime
|
||||
same-format boundaries. Sync response finish/status parity has a first strict
|
||||
pass; stream finish-reason guardrails are implemented for unknown/unmappable
|
||||
terminal reasons, while nested stream field snapshots still need
|
||||
provider-by-provider golden fixtures.
|
||||
terminal reasons. Stream event schema fields are covered in the field coverage
|
||||
matrix; provider-by-provider fixtures cover the runtime event behavior.
|
||||
|
||||
Current stream behavior:
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ Status values used below:
|
||||
|
||||
## OpenAI Reasoning Effort
|
||||
|
||||
Provider-specific type: `OpenAiReasoningEffort`.
|
||||
Provider-specific types: `OpenAiChatReasoningEffort` for Chat `reasoning_effort`, and `OpenAiResponsesReasoningEffort` for Responses `reasoning.effort`. They are intentionally separate even when their current value sets overlap; a value accepted by one field is not treated as valid for the other unless that field's own enum accepts it.
|
||||
|
||||
| Source field | Source value | Target field | Target value | Status |
|
||||
| --- | --- | --- | --- | --- |
|
||||
@@ -28,6 +28,7 @@ Provider-specific type: `OpenAiReasoningEffort`.
|
||||
| Responses `reasoning.effort` | `medium` | Chat `reasoning_effort` | `medium` | native |
|
||||
| Responses `reasoning.effort` | `high` | Chat `reasoning_effort` | `high` | native |
|
||||
| Responses `reasoning.effort` | `xhigh` | Chat `reasoning_effort` | `xhigh` | native |
|
||||
| Responses `reasoning.effort` | `max` | Chat `reasoning_effort` | none | blocked, invalid Responses enum |
|
||||
| Responses `reasoning.summary` | any | Chat | none | blocked |
|
||||
| Responses `reasoning.budget_tokens` | any | Chat | none | blocked |
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user