Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

94 changes: 94 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
.PHONY: help seed api dev build test test-unit test-integration lint fmt check-fmt preflight clean
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haven't used that one before, if it provides more benefits than makefile we can definitely switch to it


help:
@echo "NEAR AI Cloud API - Development Commands"
@echo ""
@echo "Quick Start:"
@echo " make dev Seed database and run API server (recommended)"
@echo ""
@echo "Setup & Database:"
@echo " make seed Run database migrations and seed data"
@echo ""
@echo "Running Services:"
@echo " make api Run the API server (port 3000)"
@echo ""
@echo "Code Quality:"
@echo " make preflight Run all checks before committing (lint, fmt, test-unit, build)"
@echo " make build Build all crates"
@echo " make test Run both unit and integration tests"
@echo " make test-unit Run unit tests only"
@echo " make test-integration Run integration tests only"
@echo " make lint Run clippy linter (strict mode)"
@echo " make fmt Format code with rustfmt"
@echo " make check-fmt Check formatting without fixing"
@echo ""
@echo "Cleanup:"
@echo " make clean Remove build artifacts"

## Database & Seeding

seed:
@echo "Running database migrations and seeding..."
cargo run --bin seed -p database

## Services

api:
@echo "Starting API server on http://localhost:3000"
@echo " Documentation: http://localhost:3000/docs"
@echo " OpenAPI spec: http://localhost:3000/api-docs/openapi.json"
@echo ""
cargo run --bin api

dev:
@echo "Starting development environment..."
@echo ""
cargo run --bin seed -p database && \
echo "" && \
echo "Seed complete! Starting API server on http://localhost:3000" && \
echo " Documentation: http://localhost:3000/docs" && \
echo " OpenAPI spec: http://localhost:3000/api-docs/openapi.json" && \
echo "" && \
cargo run --bin api

## Building & Testing

build:
@echo "Building all crates..."
cargo build

test-unit:
@echo "Running unit tests..."
cargo test --lib --bins

test-integration:
@echo "Running integration tests..."
cargo test --test e2e_test

test: test-unit test-integration
@echo ""
@echo "✅ All tests completed successfully!"

lint:
@echo "Running clippy linter (strict mode)..."
cargo clippy --lib --bins -- -D warnings

fmt:
@echo "Formatting code with rustfmt..."
cargo fmt

check-fmt:
@echo "Checking code formatting (without fixing)..."
cargo fmt --check

preflight: lint fmt test-unit build
@echo ""
@echo "✅ All preflight checks passed! Ready to commit."

## Cleanup

clean:
@echo "Removing build artifacts..."
cargo clean

.DEFAULT_GOAL := help
46 changes: 32 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,14 @@ A Rust-based cloud API for AI model inference, conversation management, and orga

3. **Run without Docker**:
```bash
# Set up PostgreSQL database first, then:
cargo run --bin api
make dev
```

This automatically:
- Runs all database migrations
- Seeds the database with development data
- Starts the API server on http://localhost:3000

## Testing

### Prerequisites for Testing
Expand All @@ -43,11 +47,14 @@ A Rust-based cloud API for AI model inference, conversation management, and orga
### Run Tests

```bash
# Run unit tests
cargo test --lib --bins
# Run unit tests only
make test-unit

# Run integration/e2e tests only (requires database)
make test-integration

# Run integration/e2e tests (requires database)
cargo test --test e2e_test
# Run both unit and integration tests
make test
```

### Test Database Setup
Expand All @@ -65,15 +72,15 @@ docker run --name test-postgres \
-d postgres:latest

# Run tests with default values (or override with env vars)
cargo test --test e2e_test
make test-integration

# Or with custom database settings
DATABASE_HOST=localhost \
DATABASE_PORT=5432 \
DATABASE_NAME=platform_api \
DATABASE_USERNAME=postgres \
DATABASE_PASSWORD=postgres \
cargo test --test e2e_test
make test-integration
```

#### Option 2: Using existing PostgreSQL
Expand All @@ -93,7 +100,7 @@ Copy `env.example` to `.env` and configure your test database:
```bash
cp env.example .env
# Edit .env with your database credentials
cargo test --test e2e_test
make test-integration
```

### vLLM Integration Tests
Expand All @@ -118,10 +125,21 @@ The application uses YAML configuration files located in the `config/` directory

## Contributing

1. Ensure all tests pass: `cargo test`
2. Check code formatting: `cargo fmt --check`
3. Run linting: `cargo clippy`
4. Ensure database migrations work with test setup
Before committing code:

Run all checks with a single command:

```bash
make preflight
```

This runs:
- Clippy linter (strict mode with `-D warnings`)
- Code formatting check and fix
- Unit tests
- Full build

Once all checks pass, you're ready to commit!


## API Documentation
Expand All @@ -135,4 +153,4 @@ The documentation is generated from Rust code using [utoipa](https://github.com/

## License

Licensed under the [PolyForm Strict License 1.0.0](LICENSE).
Licensed under the [PolyForm Strict License 1.0.0](LICENSE).
22 changes: 20 additions & 2 deletions crates/api/src/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1491,11 +1491,20 @@ pub struct UpdateModelApiRequest {
#[serde(rename = "isActive")]
pub is_active: Option<bool>,
pub aliases: Option<Vec<String>>,
#[serde(rename = "changeReason", skip_serializing_if = "Option::is_none")]
pub change_reason: Option<String>,
}

/// Batch update request format - Array of model name to update data
pub type BatchUpdateModelApiRequest = std::collections::HashMap<String, UpdateModelApiRequest>;

/// Delete model request - optional reason for deletion
#[derive(Debug, Deserialize, Serialize, ToSchema)]
pub struct DeleteModelRequest {
#[serde(rename = "changeReason", skip_serializing_if = "Option::is_none")]
pub change_reason: Option<String>,
}

/// Model history entry - includes pricing, context length, and other model attributes
#[derive(Debug, Serialize, Deserialize, ToSchema)]
pub struct ModelHistoryEntry {
Expand All @@ -1508,16 +1517,25 @@ pub struct ModelHistoryEntry {
pub output_cost_per_token: DecimalPrice,
#[serde(rename = "contextLength")]
pub context_length: i32,
#[serde(rename = "modelName")]
pub model_name: String,
#[serde(rename = "modelDisplayName")]
pub model_display_name: String,
#[serde(rename = "modelDescription")]
pub model_description: String,
#[serde(rename = "modelIcon")]
pub model_icon: Option<String>,
pub verifiable: bool,
#[serde(rename = "isActive")]
pub is_active: bool,
#[serde(rename = "effectiveFrom")]
pub effective_from: String,
#[serde(rename = "effectiveUntil")]
pub effective_until: Option<String>,
#[serde(rename = "changedBy")]
pub changed_by: Option<String>,
#[serde(rename = "changedByUserId")]
pub changed_by_user_id: Option<String>,
#[serde(rename = "changedByUserEmail")]
pub changed_by_user_email: Option<String>,
#[serde(rename = "changeReason")]
pub change_reason: Option<String>,
#[serde(rename = "createdAt")]
Expand Down
38 changes: 31 additions & 7 deletions crates/api/src/routes/admin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ use crate::middleware::AdminUser;
use crate::models::{
AdminAccessTokenResponse, AdminUserOrganizationDetails, AdminUserResponse,
BatchUpdateModelApiRequest, CreateAdminAccessTokenRequest, DecimalPrice,
DeleteAdminAccessTokenRequest, ErrorResponse, ListUsersResponse, ModelHistoryEntry,
ModelHistoryResponse, ModelMetadata, ModelWithPricing, OrgLimitsHistoryEntry,
OrgLimitsHistoryResponse, SpendLimit, UpdateOrganizationLimitsRequest,
DeleteAdminAccessTokenRequest, DeleteModelRequest, ErrorResponse, ListUsersResponse,
ModelHistoryEntry, ModelHistoryResponse, ModelMetadata, ModelWithPricing,
OrgLimitsHistoryEntry, OrgLimitsHistoryResponse, SpendLimit, UpdateOrganizationLimitsRequest,
UpdateOrganizationLimitsResponse,
};
use axum::{
Expand Down Expand Up @@ -50,7 +50,7 @@ pub struct AdminAppState {
)]
pub async fn batch_upsert_models(
State(app_state): State<AdminAppState>,
Extension(_admin_user): Extension<AdminUser>, // Require admin auth
Extension(admin_user): Extension<AdminUser>, // Require admin auth
ResponseJson(batch_request): ResponseJson<BatchUpdateModelApiRequest>,
) -> Result<ResponseJson<Vec<ModelWithPricing>>, (StatusCode, ResponseJson<ErrorResponse>)> {
debug!(
Expand All @@ -69,6 +69,10 @@ pub async fn batch_upsert_models(
));
}

// Extract admin user context for audit tracking
let admin_user_id = admin_user.0.id;
let admin_user_email = admin_user.0.email.clone();

// Convert API request to service request
let models = batch_request
.iter()
Expand All @@ -85,6 +89,9 @@ pub async fn batch_upsert_models(
verifiable: request.verifiable,
is_active: request.is_active,
aliases: request.aliases.clone(),
change_reason: request.change_reason.clone(),
changed_by_user_id: Some(admin_user_id),
changed_by_user_email: Some(admin_user_email.clone()),
},
)
})
Expand Down Expand Up @@ -236,11 +243,16 @@ pub async fn get_model_history(
currency: "USD".to_string(),
},
context_length: h.context_length,
model_name: h.model_name,
model_display_name: h.model_display_name,
model_description: h.model_description,
model_icon: h.model_icon,
verifiable: h.verifiable,
is_active: h.is_active,
effective_from: h.effective_from.to_rfc3339(),
effective_until: h.effective_until.map(|dt| dt.to_rfc3339()),
changed_by: h.changed_by,
changed_by_user_id: h.changed_by_user_id.map(|id| id.to_string()),
changed_by_user_email: h.changed_by_user_email,
change_reason: h.change_reason,
created_at: h.created_at.to_rfc3339(),
})
Expand Down Expand Up @@ -486,6 +498,7 @@ pub async fn get_organization_limits_history(
params(
("model_name" = String, Path, description = "Model name to delete (URL-encode if it contains slashes)")
),
request_body = DeleteModelRequest,
responses(
(status = 204, description = "Model deleted successfully"),
(status = 404, description = "Model not found", body = ErrorResponse),
Expand All @@ -499,13 +512,24 @@ pub async fn get_organization_limits_history(
pub async fn delete_model(
State(app_state): State<AdminAppState>,
Path(model_name): Path<String>,
Extension(_admin_user): Extension<AdminUser>,
Extension(admin_user): Extension<AdminUser>,
request: Option<ResponseJson<DeleteModelRequest>>,
) -> Result<StatusCode, (StatusCode, ResponseJson<ErrorResponse>)> {
debug!("Delete model request for: {}", model_name);

// Extract admin user context for audit tracking
let admin_user_id = admin_user.0.id;
let admin_user_email = admin_user.0.email.clone();
let change_reason = request.and_then(|ResponseJson(req)| req.change_reason);

app_state
.admin_service
.delete_model(&model_name)
.delete_model(
&model_name,
change_reason,
Some(admin_user_id),
Some(admin_user_email),
)
.await
.map_err(|e| {
error!("Failed to delete model");
Expand Down
Loading