mirror of
https://github.com/fawney19/Aether.git
synced 2026-09-10 21:20:20 +08:00
fix: preserve provider schema drift safely
This commit is contained in:
@@ -2785,6 +2785,59 @@ mod tests {
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn same_format_canonical_roundtrip_preserves_future_root_fields() {
|
||||
let cases = [
|
||||
(
|
||||
"openai:chat",
|
||||
json!({
|
||||
"model": "gpt-source",
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
"future_field": {"enabled": true}
|
||||
}),
|
||||
"future_field",
|
||||
),
|
||||
(
|
||||
"openai:responses",
|
||||
json!({
|
||||
"model": "gpt-source",
|
||||
"input": [{"role": "user", "content": "hello"}],
|
||||
"future_field": {"enabled": true}
|
||||
}),
|
||||
"future_field",
|
||||
),
|
||||
(
|
||||
"claude:messages",
|
||||
json!({
|
||||
"model": "claude-source",
|
||||
"max_tokens": 1024,
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
"future_field": {"enabled": true}
|
||||
}),
|
||||
"future_field",
|
||||
),
|
||||
(
|
||||
"gemini:generate_content",
|
||||
json!({
|
||||
"model": "gemini-source",
|
||||
"contents": [{"role": "user", "parts": [{"text": "hello"}]}],
|
||||
"futureField": {"enabled": true}
|
||||
}),
|
||||
"futureField",
|
||||
),
|
||||
];
|
||||
|
||||
for (format, body, field) in cases {
|
||||
let converted = convert_request_pure(format, format, &body)
|
||||
.unwrap_or_else(|err| panic!("{format} same-format roundtrip failed: {err}"));
|
||||
assert_eq!(
|
||||
converted.value.get(field),
|
||||
body.get(field),
|
||||
"{format} must preserve unknown provider root fields in canonical roundtrip"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pure_openai_chat_to_claude_blocks_target_unsupported_generation_field() {
|
||||
let body = json!({
|
||||
|
||||
@@ -4582,9 +4582,29 @@ pub(crate) fn apply_gemini_request_extensions(
|
||||
if let Some(raw_tool_config) = gemini.get("raw_tool_config").cloned() {
|
||||
output_object.insert("toolConfig".to_string(), raw_tool_config);
|
||||
}
|
||||
for (key, value) in gemini {
|
||||
if GEMINI_REQUEST_EXTENSION_INTERNAL_KEYS.contains(&key.as_str()) {
|
||||
continue;
|
||||
}
|
||||
output_object
|
||||
.entry(key.clone())
|
||||
.or_insert_with(|| value.clone());
|
||||
}
|
||||
Some(())
|
||||
}
|
||||
|
||||
const GEMINI_REQUEST_EXTENSION_INTERNAL_KEYS: &[&str] = &[
|
||||
"builtin_tools",
|
||||
"cached_content",
|
||||
"generation_config_extra",
|
||||
"grounding",
|
||||
"raw_tool_config",
|
||||
"raw_tools",
|
||||
"response_modalities",
|
||||
"safety_settings",
|
||||
"thinking_config",
|
||||
];
|
||||
|
||||
fn should_reuse_raw_gemini_tools(gemini: &Map<String, Value>) -> bool {
|
||||
let Some(google_search) = gemini
|
||||
.get("grounding")
|
||||
|
||||
@@ -13,7 +13,9 @@ Statuses:
|
||||
- `lossy-blocked`: conversion fails closed.
|
||||
- `invalid-enum`: conversion fails closed because a provider enum value is not valid for the target mapping.
|
||||
|
||||
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` by `python3 docs/api/generate_format_field_coverage.py` 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.
|
||||
Full schema field coverage is tracked in `docs/api/format-field-coverage-matrix.md`. That matrix is generated from the schema inventory in `docs/api/provider-interface-definitions.md` by `python3 docs/api/generate_format_field_coverage.py` 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.
|
||||
|
||||
Provider schema refresh is not a runtime dependency. Same-format runtime paths do not use this matrix; they bypass canonical conversion. Same-format canonical roundtrip must preserve unrecognized provider fields through provider extension namespaces. Cross-format conversion is capability-based: only explicitly mapped fields are emitted, and newly discovered or unknown provider fields fail closed until a lossless mapping is audited.
|
||||
|
||||
## Implemented Boundary Changes
|
||||
|
||||
@@ -26,6 +28,7 @@ Full schema field coverage is tracked in `docs/api/format-field-coverage-matrix.
|
||||
| Conversion errors | Added `UnsupportedField`, `InvalidEnumValue`, `LossyConversionBlocked`, and `InvalidTargetField`. |
|
||||
| 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. |
|
||||
| Schema drift handling | Official schema changes are detected by regenerating the inventory/matrix. Runtime same-format remains passthrough; cross-format unknowns remain blocked until deliberately mapped. |
|
||||
| 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. |
|
||||
|
||||
|
||||
@@ -2,7 +2,9 @@
|
||||
|
||||
Last generated: 2026-06-03
|
||||
|
||||
This file is generated from `docs/api/provider-interface-definitions.md` and gives every documented schema field an explicit handling status. “处理到” here means the field is either mapped, preserved in same-format paths, rejected with a structured fail-closed error, or explicitly outside the current conversion surface. It does not mean every field can be cross-format converted.
|
||||
This file is generated from the schema inventory in `docs/api/provider-interface-definitions.md` and gives every documented schema field an explicit handling status. “处理到” here means the field is either mapped, preserved in same-format paths, rejected with a structured fail-closed error, or explicitly outside the current conversion surface. It does not mean every field can be cross-format converted.
|
||||
|
||||
Provider schema updates do not require immediate conversion-code changes for runtime safety. Same-format runtime paths bypass canonical conversion, and same-format canonical roundtrip preserves provider extension fields. Cross-format conversion only enables fields with an audited semantic mapping; newly discovered or unknown provider fields default to structured fail-closed behavior until mapped.
|
||||
|
||||
Regenerate with: `python3 docs/api/generate_format_field_coverage.py`.
|
||||
|
||||
|
||||
@@ -20,6 +20,8 @@ Important limitation:
|
||||
- The current transport helper receives `body_json: &serde_json::Value`, not raw request bytes. It therefore guarantees no canonical conversion and JSON value preservation at this layer, but it cannot preserve original whitespace or object key order by itself.
|
||||
- True byte-level passthrough for requests with no transport edits requires a higher-level raw-body path that can forward the original bytes directly. Until that raw-body plumbing exists, tests should assert "conversion module not called" and JSON value equivalence for this helper, not byte-for-byte serialization equivalence.
|
||||
|
||||
Provider schema drift does not change this rule. If OpenAI, Claude, or Gemini add a new field, same-format runtime routing must still forward it as part of the original provider body. The schema inventory and field coverage matrix are audit aids, not the runtime allowlist for same-format traffic.
|
||||
|
||||
## Canonical Same-Format Roundtrip
|
||||
|
||||
Canonical same-format roundtrip is only a test/audit mode:
|
||||
@@ -33,6 +35,7 @@ Required behavior:
|
||||
- JSON-normalized equality, ignoring object field order and whitespace.
|
||||
- Field values, array order, unknown fields, extension namespaces, and unknown enum strings must be preserved.
|
||||
- This path may parse and emit; it is not the runtime path.
|
||||
- Unknown provider fields are carried in provider extension namespaces and replayed when emitting the same provider format.
|
||||
|
||||
## Cross-Format Conversion
|
||||
|
||||
@@ -49,6 +52,7 @@ Required behavior:
|
||||
- Preserve source fields only when the target has an equivalent field or documented extension passthrough.
|
||||
- Fail closed with `FormatError::LossyConversionBlocked`, `FormatError::UnsupportedField`, `FormatError::InvalidEnumValue`, or `FormatError::InvalidTargetField` when no lossless mapping exists.
|
||||
- Do not use `None` or silent omission to represent conversion failure.
|
||||
- Newly added provider fields follow the same rule as other unknown fields: preserve same-format, fail closed cross-format. A code change is required only when Aether intentionally supports a new cross-format semantic mapping.
|
||||
|
||||
## Pure Conversion Interface
|
||||
|
||||
@@ -79,4 +83,3 @@ Current pure entrypoints:
|
||||
- `convert_response_pure`
|
||||
|
||||
`convert_request` and `convert_response` remain legacy wrappers for existing callers that still need mapped model/report-context behavior during migration.
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate the provider schema field coverage matrix.
|
||||
|
||||
The source of truth is docs/api/provider-interface-definitions.md. Existing
|
||||
The input inventory is docs/api/provider-interface-definitions.md. Existing
|
||||
coverage rows are reused so audited status/notes survive regeneration. Newly
|
||||
introduced provider fields get conservative fail-closed defaults until a human
|
||||
audits whether they deserve an explicit mapping.
|
||||
introduced provider fields get conservative same-format/native and cross-format
|
||||
fail-closed defaults until a human audits whether they deserve an explicit
|
||||
mapping.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
@@ -476,7 +477,9 @@ def render_matrix(
|
||||
"",
|
||||
"Last generated: 2026-06-03",
|
||||
"",
|
||||
"This file is generated from `docs/api/provider-interface-definitions.md` and gives every documented schema field an explicit handling status. “处理到” here means the field is either mapped, preserved in same-format paths, rejected with a structured fail-closed error, or explicitly outside the current conversion surface. It does not mean every field can be cross-format converted.",
|
||||
"This file is generated from the schema inventory in `docs/api/provider-interface-definitions.md` and gives every documented schema field an explicit handling status. “处理到” here means the field is either mapped, preserved in same-format paths, rejected with a structured fail-closed error, or explicitly outside the current conversion surface. It does not mean every field can be cross-format converted.",
|
||||
"",
|
||||
"Provider schema updates do not require immediate conversion-code changes for runtime safety. Same-format runtime paths bypass canonical conversion, and same-format canonical roundtrip preserves provider extension fields. Cross-format conversion only enables fields with an audited semantic mapping; newly discovered or unknown provider fields default to structured fail-closed behavior until mapped.",
|
||||
"",
|
||||
"Regenerate with: `python3 docs/api/generate_format_field_coverage.py`.",
|
||||
"",
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
|
||||
本文档整理 Aether 当前接入和转换矩阵实际涉及的三类 provider 接口面:OpenAI Chat Completions / Responses / Embeddings / Images,Claude Messages,以及 Gemini GenerateContent / Embeddings / Files / PredictLongRunning 相关接口。它不是三家公司所有管理类、训练类、账单类 API 的全集。
|
||||
|
||||
这是 schema inventory / audit input,不是运行时代码的字段 allowlist。Provider 官方新增字段时,同格式运行时路径仍按原始 body 透传;canonical same-format roundtrip 通过 provider extension 保留未映射字段;跨格式转换只有在存在显式语义映射时才开放,否则 fail closed。刷新本文档只用于更新审计基线和决定是否新增跨格式映射。
|
||||
|
||||
## 来源与范围
|
||||
|
||||
| Provider | 结构化来源 | 官方参考 | 本文档覆盖 |
|
||||
|
||||
Reference in New Issue
Block a user