refactor: migrate monolithic crate to Cargo Workspace with Clean Architecture
Transform the single binary crate into a 9-crate workspace monorepo: - Root Cargo.toml as [workspace] manager with resolver = "2" - zesdex-entities: Domain entity types (session, settings, store, message, etc.) - zesdex-utils: Pure utility functions (error, logger, pagination, slug, clipboard) - zesdex-dto: Data Transfer Objects for LLM provider API communication - zesdex-ipc: Unix-socket IPC layer (client/server/framing/protocol) - zesdex-iam: Identity & Access Management (Clean Architecture: domain/application/infrastructure) - zesdex-cms: Content Management (Clean Architecture: domain/application/infrastructure) - zesdex-middleware: HTTP middleware (Auth, CORS, Rate Limiting) - zesdex-libs: Composition root (AppContext, DB init, JWT, Argon2) - zesdex-backend: Main binary entry point + seed/migrate binaries - DevOps: Dockerfile, docker-compose, Nix (flake/shell/default), CI/CD updates - Remove dead root src/ and src-misc/ directories All crate re-exports maintain backward compatibility with original crate::model::*, crate::dto::*, crate::ipc::* module paths. Feature crates enforce strict layer separation: domain -> application -> infrastructure with generic trait-based dependency injection.
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
#![allow(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_sign_loss,
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_possible_wrap
|
||||
)]
|
||||
//! Re-exports from zesdex-entities crate for canonical ChatMessage/Role types.
|
||||
|
||||
pub use zesdex_entities::seaorm::common::message::*;
|
||||
@@ -0,0 +1,11 @@
|
||||
#![allow(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_sign_loss,
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_possible_wrap
|
||||
)]
|
||||
|
||||
//! Chat DTO submodules: message roles/content and tool-call structures.
|
||||
|
||||
pub mod message;
|
||||
pub mod tool;
|
||||
@@ -0,0 +1,10 @@
|
||||
#![allow(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_sign_loss,
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_possible_wrap
|
||||
)]
|
||||
//! Re-exports from zesdex-entities crate for canonical ToolCall/ToolResult types.
|
||||
|
||||
pub use zesdex_entities::seaorm::common::tool_call::*;
|
||||
pub use zesdex_entities::seaorm::common::tool_result::*;
|
||||
@@ -0,0 +1,16 @@
|
||||
#![allow(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_sign_loss,
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_possible_wrap
|
||||
)]
|
||||
|
||||
//! Data transfer objects for LLM provider API communication and chat message
|
||||
//! wire formats.
|
||||
//!
|
||||
//! Sub-modules:
|
||||
//! - [`chat`] — `ChatMessage`, `Role`, `ToolCall`, `ToolResult`
|
||||
//! - [`provider`] — `ChatCompletionRequest`, `ChatCompletionResponse`, `TokenUsage`
|
||||
|
||||
pub mod chat;
|
||||
pub mod provider;
|
||||
@@ -0,0 +1,12 @@
|
||||
#![allow(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_sign_loss,
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_possible_wrap
|
||||
)]
|
||||
|
||||
//! Provider-facing DTOs: chat completion request, response, and usage/cost.
|
||||
|
||||
pub mod request;
|
||||
pub mod response;
|
||||
pub mod usage;
|
||||
@@ -0,0 +1,81 @@
|
||||
#![allow(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_sign_loss,
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_possible_wrap
|
||||
)]
|
||||
|
||||
//! Outbound request DTOs for the OpenAI/Anthropic-compatible chat completions API.
|
||||
//!
|
||||
//! Flow: harness/runtime builds a [`ChatCompletionRequest`] from conversation
|
||||
//! state and the active tool set → serializes to JSON via `serde` → sends to
|
||||
//! the provider's `/chat/completions`-style endpoint (streaming or not).
|
||||
//!
|
||||
//! Why: fields mirror the wire format exactly (including `#[serde(rename)]`
|
||||
//! for reserved words like `type`) so no manual (de)serialization glue is
|
||||
//! needed; optional fields use `skip_serializing_if` so unset knobs are
|
||||
//! omitted rather than sent as `null`, matching provider expectations.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
|
||||
/// Outbound chat completion request body sent to an
|
||||
/// OpenAI/Anthropic-compatible provider.
|
||||
///
|
||||
/// Flow: constructed from the current message history plus optional
|
||||
/// generation knobs (temperature, `max_tokens`, tools, etc.) and serialized
|
||||
/// directly into the HTTP request body.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ChatCompletionRequest {
|
||||
pub model: String,
|
||||
pub messages: Vec<super::super::chat::message::ChatMessage>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub max_tokens: Option<u32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub temperature: Option<f32>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stream: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stream_options: Option<StreamOptions>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tools: Option<Vec<ToolDef>>,
|
||||
/// Controls which (if any) function is called by the model.
|
||||
/// Can be `"none"`, `"auto"`, or `{"type": "function", "function": {"name": "..."}}`.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tool_choice: Option<Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub stop: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
/// Streaming options for the request; `include_usage` asks the provider to
|
||||
/// emit a final usage chunk in the SSE stream.
|
||||
///
|
||||
/// Why: usage tokens are otherwise unavailable in a streamed response since
|
||||
/// they are normally only attached to the final non-streamed completion.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct StreamOptions {
|
||||
pub include_usage: bool,
|
||||
}
|
||||
|
||||
/// Wire format for a single tool definition sent to the provider.
|
||||
///
|
||||
/// Flow: built from the harness's registered `Tool` implementations
|
||||
/// and attached to [`ChatCompletionRequest::tools`] so the model knows which
|
||||
/// functions it may call.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolDef {
|
||||
#[serde(rename = "type")]
|
||||
pub type_: String,
|
||||
pub function: ToolFunctionDef,
|
||||
}
|
||||
|
||||
/// Name, description, and JSON schema parameters for a tool definition.
|
||||
///
|
||||
/// Why: `parameters` is a raw `serde_json::Value` rather than a typed struct
|
||||
/// because each tool defines its own arbitrary JSON schema.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ToolFunctionDef {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub parameters: Value,
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
#![allow(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_sign_loss,
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_possible_wrap
|
||||
)]
|
||||
|
||||
//! Inbound response DTOs for the non-streaming chat completions API.
|
||||
//!
|
||||
//! Flow: provider HTTP response body → `serde_json` deserializes into
|
||||
//! [`ChatCompletionResponse`] → caller reads `choices[0]` for the assistant
|
||||
//! reply and `usage` for token accounting.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Non-streaming chat completion response returned by the provider.
|
||||
///
|
||||
/// Flow: deserialized directly from the HTTP response body of a
|
||||
/// non-streaming completion call.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct ChatCompletionResponse {
|
||||
pub id: String,
|
||||
pub object: String,
|
||||
pub created: i64,
|
||||
pub model: String,
|
||||
pub choices: Vec<Choice>,
|
||||
pub usage: Option<super::usage::TokenUsage>,
|
||||
}
|
||||
|
||||
/// One completion candidate within a [`ChatCompletionResponse::choices`] list.
|
||||
///
|
||||
/// For non-streaming responses the `message` field is populated; for streaming
|
||||
/// responses the `delta` field carries the incremental token.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Choice {
|
||||
pub index: u32,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub message: Option<super::super::chat::message::ChatMessage>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub delta: Option<Delta>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub finish_reason: Option<String>,
|
||||
}
|
||||
|
||||
/// Incremental delta emitted in a streaming SSE chunk.
|
||||
///
|
||||
/// Only populated when the response is streamed; `role` typically appears
|
||||
/// only on the first chunk and `content` / `tool_calls` are appended
|
||||
/// incrementally.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Delta {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub role: Option<super::super::chat::message::Role>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub content: Option<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub tool_calls: Option<Vec<super::super::chat::tool::ToolCall>>,
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
#![allow(
|
||||
clippy::cast_possible_truncation,
|
||||
clippy::cast_sign_loss,
|
||||
clippy::cast_precision_loss,
|
||||
clippy::cast_possible_wrap
|
||||
)]
|
||||
|
||||
//! Token usage accounting DTO shared by streaming and non-streaming responses.
|
||||
//!
|
||||
//! Flow: populated from the provider's `usage` object (either the final SSE
|
||||
//! chunk when `stream_options.include_usage` is set, or the `usage` field of
|
||||
//! a non-streaming [`ChatCompletionResponse`](super::response::ChatCompletionResponse))
|
||||
//! → surfaced to the TUI for cost/token display.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Token counts for a single completion request.
|
||||
///
|
||||
/// Why: these are the standard fields reported by the OpenAI-compatible chat
|
||||
/// completions API. All fields are required when present — use `Option` at
|
||||
/// the [`ChatCompletionResponse`](super::response::ChatCompletionResponse) level
|
||||
/// if usage is absent.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
|
||||
pub struct TokenUsage {
|
||||
pub prompt_tokens: u32,
|
||||
pub completion_tokens: u32,
|
||||
pub total_tokens: u32,
|
||||
}
|
||||
Reference in New Issue
Block a user