Skip to content
Merged
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
8 changes: 6 additions & 2 deletions .agents/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ Roadmap.md always has these sections in this order:

Every milestone must have at minimum: **Goal**, **What was done**, and **Key decisions**. Optional subsections: **Known issues / follow-ups**, **Tests**.

## Documentation-First Rule
## Product Development Workflow

Read `PRD.md`, `Architecture.md`, `Rules.md`, and `Phases.md` before implementing any feature. If a proposed change adds scope, update documentation first — code changes follow doc changes, not the other way around.
Read the relevant current documents in `Documentation/`: `PRD.md`, `Architecture.md`, `Phases.md`, and `Development.md`. Update product/design documentation before or alongside meaningful scope changes. Routine implementation choices and fixes do not require a new approval or planning ceremony.

The deleted rules file is retired. Its fixed library allowlist, MVP-only scope restrictions, exact folder prescriptions, and one-phase-at-a-time execution rule no longer apply. Phases define dependencies and release gates; independent work may proceed concurrently once contracts are settled. Choose tools and libraries using maintainability, compatibility, security, and measured need.

Keep current implementation, planned capabilities, and measured release evidence distinct. Preserve the historical milestones and maintain Roadmap as described above. Actual environment permissions and user authorization still govern production, destructive, and externally charged actions.
8 changes: 7 additions & 1 deletion .dockerignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,10 @@
target/
target/*
!target/ai-inference-gateway-0.0.1-SNAPSHOT.jar
.build-cache/
*.log
.agents/
.codex/
.github/
.git/
.gitignore
.idea/
Expand Down
32 changes: 12 additions & 20 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,24 +1,16 @@
# ── PostgreSQL ────────────────────────────────────────
# Local Compose bootstrap only; do not use these demo credentials in production.
POSTGRES_USER=gateway
POSTGRES_PASSWORD=gateway
POSTGRES_DB=gateway_db
LOCAL_API_KEY=test-api-key-1

# ── Gateway Database Connection ──────────────────────
DB_HOST=postgres
DB_PORT=5432
DB_NAME=gateway_db
DB_USERNAME=gateway
DB_PASSWORD=gateway

# ── Ollama ───────────────────────────────────────────
OLLAMA_BASE_URL=http://ollama:11434

# ── Test API Key (plaintext) ─────────────────────────
# The seeded API key hash in the database corresponds to this plaintext value.
# Use this in the X-API-Key header when testing locally.
TEST_API_KEY=test-api-key-1

# ── Notes ────────────────────────────────────────────
# 1. Copy this file to .env before running: cp .env.example .env
# 2. After first `docker compose up`, pull the Ollama model:
# docker exec ollama ollama pull qwen3
# Running the JAR directly: explicitly use SPRING_PROFILES_ACTIVE=local for demos.
# Production is the default profile and requires DB_PASSWORD and existing non-demo keys.
# Direct-JAR infrastructure properties:
# DB_HOST=localhost
# DB_PORT=5432
# DB_NAME=gateway_db
# DB_USERNAME=gateway
# DB_PASSWORD=<unique password>
# Local profile updates the seeded provider URL from this property:
# OLLAMA_BASE_URL=http://localhost:11434
6 changes: 6 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
* text=auto
mvnw text eol=lf
*.sh text eol=lf
*.cmd text eol=crlf
*.yml text eol=lf
*.sql text eol=lf
87 changes: 87 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
name: Foundation verification
on:
push:
pull_request:
workflow_dispatch:
permissions:
contents: read
concurrency:
group: foundation-${{ github.ref }}
cancel-in-progress: true
jobs:
verify:
runs-on: ubuntu-24.04
timeout-minutes: 25
services:
postgres:
image: postgres:16-alpine@sha256:721873c34ceb9f8d8fc265984940dc982404c105f19ad51be9fdc5970a6080ea
env:
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ci-disposable-only
POSTGRES_DB: gateway_test
ports: ["5432:5432"]
options: >-
--health-cmd pg_isready
--health-interval 5s
--health-timeout 5s
--health-retries 10
env:
TEST_DATABASE_URL: jdbc:postgresql://localhost:5432/gateway_test
TEST_DATABASE_USER: postgres
TEST_DATABASE_PASSWORD: ci-disposable-only
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5
with:
persist-credentials: false
- uses: actions/setup-java@b6effb05e454b25005698d916606bdc6ffcbf961 # v5
with:
distribution: temurin
java-version: '21'
cache: maven
- name: Build and test including PostgreSQL migrations
run: sh mvnw -B -Ppostgres-it verify
- name: Artifact manifest
run: python3 scripts/release_manifest.py target/ai-inference-gateway-0.0.1-SNAPSHOT.jar
- name: Build container
run: docker build --target packaged -t gateway:ci .
- name: Dependency secret and configuration scan
uses: aquasecurity/trivy-action@d2a0b60797ff03db6132bd4e2b293f9b37081297
with:
scan-type: fs
version: v0.74.0
scan-ref: .
scanners: vuln,secret,misconfig
severity: HIGH,CRITICAL
exit-code: '1'
skip-dirs: .git,.build-cache,.cache
format: json
output: target/source-scan.json
- name: Container vulnerability scan
uses: aquasecurity/trivy-action@d2a0b60797ff03db6132bd4e2b293f9b37081297
with:
image-ref: gateway:ci
version: v0.74.0
severity: HIGH,CRITICAL
exit-code: '1'
format: json
output: target/image-scan.json
- name: Container SBOM
uses: aquasecurity/trivy-action@d2a0b60797ff03db6132bd4e2b293f9b37081297
with:
image-ref: gateway:ci
version: v0.74.0
format: cyclonedx
output: target/container-sbom.json
exit-code: '0'
- name: Preserve verification evidence
if: always()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: foundation-evidence
path: |
target/surefire-reports/
target/failsafe-reports/
target/release-manifest.json
target/container-sbom.json
target/source-scan.json
target/image-scan.json
13 changes: 12 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Maven
.build-cache/
target/
!.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/

Expand All @@ -9,6 +9,15 @@ target/
*.jar
*.war
*.ear
# Keep the wrapper binary versionable if the wrapper distribution type changes.
!.mvn/wrapper/maven-wrapper.jar
*.hprof

# Local tooling and CI scanner caches
/.cache/
__pycache__/
*.pyc
*.pyo

# IDE - IntelliJ
.idea/
Expand All @@ -30,6 +39,8 @@ Thumbs.db

# Environment
.env
.env.*
!.env.example

# Logs
*.log
Expand Down
4 changes: 4 additions & 0 deletions .mvn/wrapper/maven-wrapper.properties
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
wrapperVersion=3.3.4
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip
distributionSha256Sum=0d7125e8c91097b36edb990ea5934e6c68b4440eef4ea96510a0f6815e7eeadb
33 changes: 18 additions & 15 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
# ── Stage 1: Build ───────────────────────────────────
FROM maven:3.9-eclipse-temurin-21 AS build
# Pinned multi-architecture base manifests; update through reviewed dependency maintenance.
FROM eclipse-temurin:21-jdk-alpine@sha256:cd87715a8d45cfaa42419207c64680234f62785c49055cccd20437b5c9018380 AS build
WORKDIR /app

# Cache dependencies first (layer caching optimization)
COPY pom.xml .
RUN mvn dependency:go-offline -B

# Copy source and build
COPY .mvn .mvn
COPY mvnw pom.xml ./
RUN chmod +x mvnw
COPY src ./src
RUN mvn package -DskipTests -B
# CI runs the full suite (including PostgreSQL) before building the image.
RUN --mount=type=cache,target=/root/.m2 ./mvnw -B -DskipTests package

# ── Stage 2: Run ─────────────────────────────────────
FROM eclipse-temurin:21-jre-alpine
FROM eclipse-temurin:21-jre-alpine@sha256:1a29e1fe337eb28b5bec30f0ee8ed29f0ff80ab6f75dcf9313efe82911065a52 AS runtime
WORKDIR /app
RUN addgroup -g 10001 gateway && adduser -D -H -u 10001 -G gateway gateway
USER 10001:10001
EXPOSE 8080 9090
ENTRYPOINT ["java", "-jar", "app.jar"]

COPY --from=build /app/target/*.jar app.jar

EXPOSE 8080
# CI packages the exact tested/manifested artifact, avoiding a second compilation.
FROM runtime AS packaged
COPY --chown=10001:10001 target/ai-inference-gateway-0.0.1-SNAPSHOT.jar app.jar

ENTRYPOINT ["java", "-jar", "app.jar"]
# Default local Compose build remains self-contained from source.
FROM runtime AS final
COPY --from=build --chown=10001:10001 /app/target/ai-inference-gateway-0.0.1-SNAPSHOT.jar app.jar
105 changes: 105 additions & 0 deletions Documentation/API.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
# API contracts and compatibility plan

Updated 2026-09-21. The first section describes current Phase 6 behavior. Sections marked planned describe future contracts. This is a design contract, not a generated OpenAPI specification. Phase 7 produces machine-readable schemas and wire fixtures; Phase 12 completes admin schemas.

Legacy preservation below is the default transition design. Under the product owner's redesign authorization, a documented replacement can revise it when preserving demo behavior would perpetuate defects or unnecessary complexity. Assess actual consumers, define the replacement/migration, update examples and tests, and revise the relevant release gates before shipping. Internal implementation changes alone do not require wire compatibility work. See [Development.md](Development.md).

## Current endpoints

| Method and path | Authentication | Current contract |
|---|---|---|
| `POST /v1/inference` | `X-API-Key` | Required nonblank `model` and `prompt`; optional `provider`; synchronous generation |
| `GET /v1/providers` | `X-API-Key` | Array of `name`, `type`, `models`; no provider base URL |
| `GET /v1/logs` | `X-API-Key` | Pagination envelope: `content`, `page`, `size`, `totalElements`, `totalPages` |
| `GET :9090/actuator/health` | None; private listener | Minimal status only; liveness/readiness subpaths supported |
| `GET :9090/actuator/prometheus` | None; private listener | Operational metrics; blocked on the public listener |

Current log filters are `provider`, `status`, `from`, `to`, `page` (default 0), and `size` (default 20, upper cap 100). Times are ISO local date-times without an offset. Negative page, nonpositive size, status outside `SUCCESS`/`FAILURE`, malformed timestamps and reversed ranges return 400. Inference requires JSON, nonblank model/prompt, maximum 200-character model/provider and 262,144-character prompt, within the 1 MiB body limit.

Phase 6 deliberately corrects provider-unavailable from 502 to 503 and returns fixed safe error messages rather than raw parser/provider text. Malformed input returns 400; missing/invalid key 401; forbidden access 403; oversized body 413; unsupported media 415; unexpected failure 500; local saturation/upstream unavailability 503; provider/deadline timeout 504. Stable error codes distinguish `OVERLOADED` and `PROVIDER_UNAVAILABLE`. Requests rejected before inference use metrics, not database diagnostic rows. No automatic retries or rate-limit accounting is implied by these statuses.

Legacy inference example:

```json
{"model":"qwen3","prompt":"What is 2+2?","provider":"ollama-local"}
```

Legacy success fields are `requestId`, `text`, `model`, `provider`, `latencyMs`. Legacy error fields are `timestamp`, `status`, `error`, `message`, `path`, `requestId`. Every request handled by the ID filter gets `X-Request-Id`; not every success envelope embeds it. README and `http/requests.http` describe the runnable MVP.

## Planned inference endpoints

| Endpoint | Delivery | Contract scope |
|---|---|---|
| `POST /v1/chat/completions` | Phase 7 | Chat messages, text, SSE, tool declarations/results and structured output where deployment supports them |
| `POST /v1/embeddings` | Phase 7 | Text/string-array inputs, vectors, model and usage; no claim of universal dimensions/encoding support |
| `GET /v1/models` | Phase 7; tenant-scoped in Phase 8 | Discover only authorized virtual model aliases |
| `POST /v1/inference` | Retained | Existing shape through first GA series; internal legacy facade |
| `POST /v1/responses` | Phase 14 | Native Responses contract after state/tool/storage semantics are designed |
| `POST /v1/messages` | Phase 14 | Native Anthropic Messages contract; separate from Phase 7 upstream adapter |

New SDK-compatible routes use `Authorization: Bearer <gateway-key>`. The bearer value is a gateway credential, never an upstream provider key. Legacy routes continue accepting `X-API-Key` during migration. When both accepted headers are present, require the same credential or reject the request; do not silently choose a more privileged one. Reject oversized/malformed keys before expensive verification.

Planned Chat Completions request (illustrative alias, not seeded today):

```json
{
"model": "support-chat",
"messages": [{"role": "user", "content": "Summarize this incident."}],
"stream": true
}
```

Keep the selected protocol's normal response shape. Return gateway correlation in `X-Request-Id`; do not wrap compatible responses in the legacy envelope. Gateway-specific metadata belongs in documented headers or a namespaced extension, not arbitrary schema changes.

### Compatibility rules

- Publish a versioned matrix for role/content types, generation controls, output constraints, tools, usage and streaming. A provider adapter may support a smaller set than another.
- Preserve tool IDs, argument fragments, finish reasons and usage semantics. The GA gateway passes tool requests to the application; it does not execute tools.
- Recognized but unsupported parameters return a structured 400 before dispatch. Define an explicit unknown-field policy in schema fixtures; no silent dropping of safety or semantic fields.
- Fallback candidates must support the requested operation and options. A text-only candidate cannot receive a vision request.
- Provider passthrough, if introduced later, is an explicitly authorized endpoint with an allowlist and size limits; it cannot bypass governance.
- Publish tested client SDK versions, provider/model/API versions and unsupported cases. SDK compatibility is scoped, not universal.

### Streaming

Use `text/event-stream`, disable intermediary response buffering, and preserve ordered events. Phase 7 fixtures define the Chat Completions chunk shape, optional final usage chunk and terminal marker. A disconnect may prevent final usage delivery; persist unknown/estimated usage internally when provider totals are absent.

Before response commitment, errors use the appropriate HTTP status and protocol error envelope. After response bytes are sent, emit a safe stream error where supported and close; an already-sent 200 cannot become 503. Do not fabricate normal completion or replay another provider after any downstream bytes. Native protocols introduced later need their own terminal/error fixtures.

### Error taxonomy

| Category | Target HTTP status before commitment | Expected behavior |
|---|---|---|
| Malformed/unsupported input | 400 | Safe field-level message, no upstream call |
| Invalid/expired credential | 401 | No resource disclosure |
| Forbidden model/scope/policy | 403 | Terminal; no fallback to evade policy |
| Unknown visible model/resource | 404 | Do not reveal another tenant's object existence |
| Concurrent admin update/idempotency mismatch | 409 or 412 for failed `If-Match` | Caller refreshes; no blind overwrite |
| Too large / unsupported media | 413 / 415 | Reject before expensive work |
| Request/token/budget limit exceeded | 429 with distinct stable code | `Retry-After` only when a meaningful retry time is known |
| No eligible healthy target/dependency unavailable | 503 | Bounded response; safe retry guidance |
| Total/provider timeout | 504 | No unbounded attempts |
| Unexpected internal failure | 500 | Sanitized message and correlation ID |

Internal categories map to protocol-native error bodies. Legacy routes retain their envelope. Add mappings in filters, async handlers and the global error handler; controller advice alone cannot cover every failure. Upstream credential errors are gateway deployment failures, not a 401 blaming a correctly authenticated client.

## Planned administration and query API

Use `/admin/v1` with OIDC and explicit role/scope checks. Proposed resources are `/tenants`, `/applications`, `/service-accounts`, `/keys`, `/providers`, `/deployments`, `/virtual-models`, `/route-policies`, `/budgets`, `/usage`, `/requests`, `/audit-events`, and `/config-versions`. These paths are not implemented yet.

Tenant-scoped endpoints derive authorization from the principal and validate any selected tenant. Platform-wide operations require platform role and audit. API keys are shown once at creation; subsequent representations expose metadata only. Provider credentials return masked metadata/reference IDs, never secret values.

Configuration actions on `/config-versions/{id}` include validation, simulation, staging and activation. Rollback creates an audited activation of an earlier revision. Return an operation identifier and convergence state, rather than claiming all replicas switched atomically.

New admin/event APIs use stable cursor pagination, bounded filters and UTC offset timestamps. Mutations support `ETag/If-Match` and scoped idempotency keys. Reusing an idempotency key with a different body is an error. A repeated admin request can be deduplicated; repeated inference is not guaranteed exactly once unless the operation and provider explicitly support it.

## Migration policy

1. Capture existing API fixtures before implementation and preserve legacy bodies through GA.
2. Add new protocols without redirects from `/v1/inference`.
3. Rotate legacy keys because their hashes cannot yield lookup prefixes. Publish the legacy acceptance window and disable it by default for new deployments.
4. Restrict old logs/providers endpoints by tenant and read scope. Existing globally visible records become restricted legacy history, not guessed ownership.
5. New clients use model aliases. Existing model names map explicitly when unambiguous; provider override still passes access/residency/capability checks.
6. Publish security changes and deprecation timelines before removal. Provider API changes and unsupported fields require a compatibility-matrix update and regressions.

See [Architecture.md](Architecture.md) for lifecycle, [Security.md](Security.md) for roles and [Phases.md](Phases.md) for release gates.
Loading
Loading