mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-10 03:30:59 +00:00
Simplify opaque OpenAI item handling and DockerfileSource serde
- Extract ContentPart::is_opaque_openai() to deduplicate matches! patterns - Use std::mem::take to avoid cloning reasoning/message items - Replace hand-rolled DockerfileSource serde with derive + untagged enum - Add --fail-with-body to imagegen curl for better error reporting - Add tmp to .gitignore Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
22a1bba192
commit
c62d9b68d9
7 changed files with 24 additions and 86 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -3,3 +3,4 @@ target
|
|||
.entire
|
||||
.claude
|
||||
node_modules
|
||||
tmp
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ impl History {
|
|||
timestamp: std::time::SystemTime::now(),
|
||||
});
|
||||
self.turns.extend(preserved);
|
||||
self.strip_opaque_reasoning();
|
||||
self.strip_opaque_provider_items();
|
||||
}
|
||||
|
||||
/// Remove provider-specific opaque items that are no longer valid after compaction.
|
||||
|
|
@ -35,12 +35,10 @@ impl History {
|
|||
/// responses; after compaction replaces their surrounding context with a summary, they
|
||||
/// serve no purpose and can violate API constraints (reasoning must be followed by its
|
||||
/// output, identified by the message item's `id`).
|
||||
fn strip_opaque_reasoning(&mut self) {
|
||||
fn strip_opaque_provider_items(&mut self) {
|
||||
for turn in &mut self.turns {
|
||||
if let Turn::Assistant { provider_parts, .. } = turn {
|
||||
provider_parts.retain(|p| {
|
||||
!matches!(p, ContentPart::Other { kind, .. } if kind == ContentPart::OPENAI_REASONING || kind == ContentPart::OPENAI_MESSAGE)
|
||||
});
|
||||
provider_parts.retain(|p| !p.is_opaque_openai());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -259,10 +259,7 @@ fn translate_input(messages: &[Message]) -> (Option<String>, Vec<serde_json::Val
|
|||
"arguments": args,
|
||||
}));
|
||||
}
|
||||
// Round-trip opaque items back to the API
|
||||
ContentPart::Other { kind, data }
|
||||
if kind == ContentPart::OPENAI_REASONING || kind == ContentPart::OPENAI_MESSAGE =>
|
||||
{
|
||||
ContentPart::Other { data, .. } if part.is_opaque_openai() => {
|
||||
input.push(data.clone());
|
||||
}
|
||||
_ => {}
|
||||
|
|
@ -825,17 +822,17 @@ fn handle_response_completed(
|
|||
|
||||
let mut content_parts = Vec::new();
|
||||
// Reasoning items must precede function calls for Responses API round-trip
|
||||
for item in &state.reasoning_items {
|
||||
for item in std::mem::take(&mut state.reasoning_items) {
|
||||
content_parts.push(ContentPart::Other {
|
||||
kind: ContentPart::OPENAI_REASONING.to_string(),
|
||||
data: item.clone(),
|
||||
data: item,
|
||||
});
|
||||
}
|
||||
// Preserve full message output items for Responses API round-tripping
|
||||
for item in &state.message_items {
|
||||
for item in std::mem::take(&mut state.message_items) {
|
||||
content_parts.push(ContentPart::Other {
|
||||
kind: ContentPart::OPENAI_MESSAGE.to_string(),
|
||||
data: item.clone(),
|
||||
data: item,
|
||||
});
|
||||
}
|
||||
if !state.accumulated_text.is_empty() {
|
||||
|
|
|
|||
|
|
@ -231,6 +231,12 @@ impl ContentPart {
|
|||
pub fn text(text: impl Into<String>) -> Self {
|
||||
Self::Text(text.into())
|
||||
}
|
||||
|
||||
/// Returns `true` if this is an opaque OpenAI item (reasoning or message)
|
||||
/// that should be round-tripped verbatim through the API.
|
||||
pub fn is_opaque_openai(&self) -> bool {
|
||||
matches!(self, ContentPart::Other { kind, .. } if kind == Self::OPENAI_REASONING || kind == Self::OPENAI_MESSAGE)
|
||||
}
|
||||
}
|
||||
|
||||
// --- 3.1 Message ---
|
||||
|
|
|
|||
|
|
@ -185,7 +185,7 @@ fn resolve_dockerfile(config: &mut WorkflowRunConfig, config_dir: &Path) -> anyh
|
|||
.and_then(|d| d.snapshot.as_mut())
|
||||
.and_then(|snap| snap.dockerfile.as_mut());
|
||||
|
||||
if let Some(DockerfileSource::Path(ref rel)) = source {
|
||||
if let Some(DockerfileSource::Path { path: ref rel }) = source {
|
||||
let path = config_dir.join(rel);
|
||||
let contents = std::fs::read_to_string(&path)
|
||||
.with_context(|| format!("Failed to read dockerfile at {}", path.display()))?;
|
||||
|
|
@ -569,7 +569,9 @@ dockerfile = { path = "./Dockerfile" }
|
|||
let snapshot = config.sandbox.unwrap().daytona.unwrap().snapshot.unwrap();
|
||||
assert_eq!(
|
||||
snapshot.dockerfile,
|
||||
Some(DockerfileSource::Path("./Dockerfile".into()))
|
||||
Some(DockerfileSource::Path {
|
||||
path: "./Dockerfile".into()
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -133,77 +133,11 @@ impl<'de> Deserialize<'de> for DaytonaNetwork {
|
|||
/// `Path` variants are resolved to `Inline` during config loading
|
||||
/// (see `run_config::resolve_dockerfile`), so downstream consumers
|
||||
/// should only ever see `Inline`.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(untagged)]
|
||||
pub enum DockerfileSource {
|
||||
Inline(String),
|
||||
Path(String),
|
||||
}
|
||||
|
||||
impl Serialize for DockerfileSource {
|
||||
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
match self {
|
||||
DockerfileSource::Inline(s) => serializer.serialize_str(s),
|
||||
DockerfileSource::Path(p) => {
|
||||
use serde::ser::SerializeMap;
|
||||
let mut map = serializer.serialize_map(Some(1))?;
|
||||
map.serialize_entry("path", p)?;
|
||||
map.end()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for DockerfileSource {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
struct DockerfileSourceVisitor;
|
||||
|
||||
impl<'de> Visitor<'de> for DockerfileSourceVisitor {
|
||||
type Value = DockerfileSource;
|
||||
|
||||
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(formatter, r#"a string or {{ path = "..." }}"#)
|
||||
}
|
||||
|
||||
fn visit_str<E: de::Error>(self, value: &str) -> Result<DockerfileSource, E> {
|
||||
Ok(DockerfileSource::Inline(value.to_owned()))
|
||||
}
|
||||
|
||||
fn visit_map<M: MapAccess<'de>>(
|
||||
self,
|
||||
mut map: M,
|
||||
) -> Result<DockerfileSource, M::Error> {
|
||||
let Some(key) = map.next_key::<String>()? else {
|
||||
return Err(de::Error::custom(
|
||||
r#"empty table: expected { path = "..." }"#,
|
||||
));
|
||||
};
|
||||
|
||||
if key != "path" {
|
||||
return Err(de::Error::custom(format!(
|
||||
r#"unknown key "{key}": expected "path""#
|
||||
)));
|
||||
}
|
||||
|
||||
let path: String = map.next_value()?;
|
||||
|
||||
if let Some(extra) = map.next_key::<String>()? {
|
||||
return Err(de::Error::custom(format!(
|
||||
r#"unexpected key "{extra}": path table must have exactly one key"#
|
||||
)));
|
||||
}
|
||||
|
||||
Ok(DockerfileSource::Path(path))
|
||||
}
|
||||
}
|
||||
|
||||
deserializer.deserialize_any(DockerfileSourceVisitor)
|
||||
}
|
||||
Path { path: String },
|
||||
}
|
||||
|
||||
/// Snapshot configuration: when present, the sandbox is created from a snapshot
|
||||
|
|
@ -335,7 +269,7 @@ impl DaytonaSandbox {
|
|||
Err(daytona_sdk::DaytonaError::NotFound { .. }) => {
|
||||
let dockerfile = match &snap_cfg.dockerfile {
|
||||
Some(DockerfileSource::Inline(s)) => s.as_str(),
|
||||
Some(DockerfileSource::Path(_)) => {
|
||||
Some(DockerfileSource::Path { .. }) => {
|
||||
return Err(format!(
|
||||
"Snapshot '{}': dockerfile path should have been resolved to inline content before sandbox creation",
|
||||
snap_cfg.name
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ API_URL="https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:genera
|
|||
echo "Generating image for: ${PROMPT}" >&2
|
||||
echo "Model: ${MODEL}" >&2
|
||||
|
||||
RESPONSE=$(curl -s -X POST "$API_URL" \
|
||||
RESPONSE=$(curl -s --fail-with-body -X POST "$API_URL" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$(jq -n --arg prompt "$PROMPT" '{
|
||||
contents: [{ parts: [{ text: $prompt }] }],
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue