diff --git a/.github/actions/helm-deploy/action.yml b/.github/actions/helm-deploy/action.yml index cba3c14..011f8d9 100644 --- a/.github/actions/helm-deploy/action.yml +++ b/.github/actions/helm-deploy/action.yml @@ -24,47 +24,62 @@ inputs: description: 'Kubernetes namespace' required: true default: 'backend' + github_token: + description: 'GitHub token for Helm installation' + required: true runs: using: 'composite' - steps: - - name: Set up Kubernetes config - shell: bash - run: | - mkdir -p $HOME/.kube - echo "${{ inputs.kube_config_data }}" | base64 -d > $HOME/.kube/config - chmod 600 $HOME/.kube/config - + steps: - name: Install Helm uses: azure/setup-helm@v3 with: version: 'latest' + env: + GITHUB_TOKEN: ${{ inputs.github_token }} + - name: Set up Kubernetes config + shell: bash + run: | + mkdir -p $HOME/.kube + echo "${{ inputs.kube_config_data }}" > $HOME/.kube/config + chmod 600 $HOME/.kube/config + - name: Parse environment variables id: parse_env shell: bash run: | if [ -n "${{ inputs.helm_values_env }}" ]; then - echo "helm_env_values<> $GITHUB_OUTPUT + # Create temporary file to avoid exposing secrets in logs + temp_file=$(mktemp) echo "${{ inputs.helm_values_env }}" | while IFS='=' read -r key value; do # Skip commented lines and empty lines if [[ "$key" =~ ^#.*$ ]] || [ -z "$key" ]; then continue fi if [ -n "$key" ] && [ -n "$value" ]; then - echo " $key: \"$value\"" + echo "::add-mask::$value" + echo " $key: \"$value\"" >> "$temp_file" fi - done >> $GITHUB_OUTPUT + done + + # Output the parsed values without exposing them in logs + echo "helm_env_values<> $GITHUB_OUTPUT + cat "$temp_file" >> $GITHUB_OUTPUT echo "EOF" >> $GITHUB_OUTPUT + rm "$temp_file" else echo "helm_env_values=" >> $GITHUB_OUTPUT fi - + - name: Deploy with Helm shell: bash run: | - # Create temporary values file - cat > /tmp/override-values.yaml << EOF + # Create temporary values file with restricted permissions + temp_values=$(mktemp) + chmod 600 "$temp_values" + + cat > "$temp_values" << EOF image: repository: ${{ inputs.registry_repository }} tag: "${{ inputs.image_tag }}" @@ -77,13 +92,23 @@ runs: ${{ steps.parse_env.outputs.helm_env_values }} EOF - # Deploy using Helm + # Deploy using Helm (values file won't be logged due to file redirection) helm upgrade --install slm-server ./deploy/helm \ --namespace ${{ inputs.namespace }} \ --create-namespace \ - --values /tmp/override-values.yaml \ + --values "$temp_values" \ --wait \ --timeout 10m + + # Clean up temporary file + rm "$temp_values" + + - name: Cleanup on cancellation + if: cancelled() + shell: bash + run: | + echo "Workflow cancelled, attempting helm rollback..." + helm rollback slm-server 0 -n ${{ inputs.namespace }} --wait --timeout 5m || true - name: Verify deployment shell: bash diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 6ff5431..ad14bf0 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -76,3 +76,4 @@ jobs: helm_values_persistence_hostpath: ${{ secrets.HELM_VALUES_PERSISTENCE_HOSTPATH }} helm_values_persistence_nodename: ${{ secrets.HELM_VALUES_PERSISTENCE_NODENAME }} namespace: ${{ env.NAMESPACE }} + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e9eba8..a0e5a68 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,12 +36,12 @@ jobs: uv run pytest tests/ --ignore=tests/e2e/ --cov=slm_server --cov-report=xml --cov-report=term-missing - name: Upload coverage to Codecov - uses: codecov/codecov-action@v4 + uses: codecov/codecov-action@v5 with: file: ./coverage.xml flags: unittests - name: codecov-umbrella fail_ci_if_error: false + token: ${{ secrets.CODECOV_TOKEN }} - name: Lint with ruff run: | diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 7330445..14a53e2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -30,3 +30,4 @@ jobs: helm_values_persistence_hostpath: ${{ secrets.HELM_VALUES_PERSISTENCE_HOSTPATH }} helm_values_persistence_nodename: ${{ secrets.HELM_VALUES_PERSISTENCE_NODENAME }} namespace: ${{ env.NAMESPACE }} + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 8794c44..840a337 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,18 @@ -# 🤖 SLM Server +# Small-Language-Model Server [![CI Pipeline](https://github.com/XyLearningProgramming/slm_server/actions/workflows/ci.yml/badge.svg)](https://github.com/XyLearningProgramming/slm_server/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/XyLearningProgramming/slm_server/branch/main/graph/badge.svg)](https://codecov.io/gh/XyLearningProgramming/slm_server) [![Docker](https://img.shields.io/badge/docker-ready-blue.svg)](https://hub.docker.com/r/x3huang/slm_server) [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE) -> 🚀 **Production-ready FastAPI model server** for small language models with OpenAI-compatible API, built-in observability, and enterprise-grade deployment tools. +🚀 A light model server that serves small language models (default: `Qwen3-0.6B-GGUF`) as a **thin wrapper** around `llama-cpp` exposing the OpenAI-compatible `/chat/completions` API. Core logic is just <100 lines under `./slm_server/app.py`! -A light model server that serves small language models (default: `Qwen3-0.6B-GGUF`) using `llama-cpp` via the OpenAI-compatible `/chat/completions` API. Designed for resource-constrained environments with comprehensive monitoring and deployment automation. +> This is still a WIP project. Issues, pull-requests are welcome. I mainly use this repo to deploy a SLM model as part of the backend on my own site [x3huang.dev](https://x3huang.dev/) while trying my best to keep this repo model-agonistic. ## ✨ Features +![Thin wrapper around llama cpp](./docs/20250712_slm_img1.jpg) + - 🔌 **OpenAI-compatible API** - Drop-in replacement with `/chat/completions` endpoint and streaming support - ⚡ **Llama.cpp integration** - High-performance inference optimized for limited CPU and memory resources - 📊 **Production observability** - Built-in logging, Prometheus metrics, and OpenTelemetry tracing (all configurable) @@ -50,7 +52,7 @@ docker run -p 8000:8000 -v $(pwd)/models:/app/models slm_server ### Test the API ```bash -curl -X POST http://localhost:8000/chat/completions \ +curl -X POST http://localhost:8000/api/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "qwen", diff --git a/deploy/helm/templates/NOTES.txt b/deploy/helm/templates/NOTES.txt index 51c093a..9f7bd66 100644 --- a/deploy/helm/templates/NOTES.txt +++ b/deploy/helm/templates/NOTES.txt @@ -6,16 +6,16 @@ {{- end }} {{- end }} {{- else if contains "NodePort" .Values.service.type }} - export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "slm_server.fullname" . }}) + export NODE_PORT=$(kubectl get --namespace {{ .Release.Namespace }} -o jsonpath="{.spec.ports[0].nodePort}" services {{ include "slm-server.fullname" . }}) export NODE_IP=$(kubectl get nodes --namespace {{ .Release.Namespace }} -o jsonpath="{.items[0].status.addresses[0].address}") echo http://$NODE_IP:$NODE_PORT {{- else if contains "LoadBalancer" .Values.service.type }} NOTE: It may take a few minutes for the LoadBalancer IP to be available. - You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "slm_server.fullname" . }}' - export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "slm_server.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") + You can watch its status by running 'kubectl get --namespace {{ .Release.Namespace }} svc -w {{ include "slm-server.fullname" . }}' + export SERVICE_IP=$(kubectl get svc --namespace {{ .Release.Namespace }} {{ include "slm-server.fullname" . }} --template "{{"{{ range (index .status.loadBalancer.ingress 0) }}{{.}}{{ end }}"}}") echo http://$SERVICE_IP:{{ .Values.service.port }} {{- else if contains "ClusterIP" .Values.service.type }} - export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "slm_server.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") + export POD_NAME=$(kubectl get pods --namespace {{ .Release.Namespace }} -l "app.kubernetes.io/name={{ include "slm-server.name" . }},app.kubernetes.io/instance={{ .Release.Name }}" -o jsonpath="{.items[0].metadata.name}") export CONTAINER_PORT=$(kubectl get pod --namespace {{ .Release.Namespace }} $POD_NAME -o jsonpath="{.spec.containers[0].ports[0].containerPort}") echo "Visit http://127.0.0.1:8080 to use your application" kubectl --namespace {{ .Release.Namespace }} port-forward $POD_NAME 8080:$CONTAINER_PORT diff --git a/deploy/helm/templates/hpa.yaml b/deploy/helm/templates/hpa.yaml index 65ab5c3..1525c2f 100644 --- a/deploy/helm/templates/hpa.yaml +++ b/deploy/helm/templates/hpa.yaml @@ -2,14 +2,14 @@ apiVersion: autoscaling/v2 kind: HorizontalPodAutoscaler metadata: - name: {{ include "slm_server.fullname" . }} + name: {{ include "slm-server.fullname" . }} labels: - {{- include "slm_server.labels" . | nindent 4 }} + {{- include "slm-server.labels" . | nindent 4 }} spec: scaleTargetRef: apiVersion: apps/v1 kind: Deployment - name: {{ include "slm_server.fullname" . }} + name: {{ include "slm-server.fullname" . }} minReplicas: {{ .Values.autoscaling.minReplicas }} maxReplicas: {{ .Values.autoscaling.maxReplicas }} metrics: diff --git a/deploy/helm/templates/ingress.yaml b/deploy/helm/templates/ingress.yaml index d021348..6119712 100644 --- a/deploy/helm/templates/ingress.yaml +++ b/deploy/helm/templates/ingress.yaml @@ -2,9 +2,9 @@ apiVersion: networking.k8s.io/v1 kind: Ingress metadata: - name: {{ include "slm_server.fullname" . }} + name: {{ include "slm-server.fullname" . }} labels: - {{- include "slm_server.labels" . | nindent 4 }} + {{- include "slm-server.labels" . | nindent 4 }} {{- with .Values.ingress.annotations }} annotations: {{- toYaml . | nindent 4 }} @@ -35,7 +35,7 @@ spec: {{- end }} backend: service: - name: {{ include "slm_server.fullname" $ }} + name: {{ include "slm-server.fullname" $ }} port: number: {{ $.Values.service.port }} {{- end }} diff --git a/deploy/helm/templates/pv.yaml b/deploy/helm/templates/pv.yaml index 3176a64..6e2b2c5 100644 --- a/deploy/helm/templates/pv.yaml +++ b/deploy/helm/templates/pv.yaml @@ -10,7 +10,7 @@ spec: storage: {{ .Values.persistence.size }} accessModes: - {{ .Values.persistence.accessMode }} - hostPath: + local: path: {{ .Values.persistence.hostPath }} nodeAffinity: required: diff --git a/deploy/helm/templates/tests/test-connection.yaml b/deploy/helm/templates/tests/test-connection.yaml index e2d9288..4c4b28d 100644 --- a/deploy/helm/templates/tests/test-connection.yaml +++ b/deploy/helm/templates/tests/test-connection.yaml @@ -1,9 +1,9 @@ apiVersion: v1 kind: Pod metadata: - name: "{{ include "slm_server.fullname" . }}-test-connection" + name: "{{ include "slm-server.fullname" . }}-test-connection" labels: - {{- include "slm_server.labels" . | nindent 4 }} + {{- include "slm-server.labels" . | nindent 4 }} annotations: "helm.sh/hook": test spec: @@ -11,5 +11,5 @@ spec: - name: wget image: busybox command: ['wget'] - args: ['{{ include "slm_server.fullname" . }}:{{ .Values.service.port }}'] + args: ['{{ include "slm-server.fullname" . }}:{{ .Values.service.port }}'] restartPolicy: Never diff --git a/deploy/helm/values.yaml b/deploy/helm/values.yaml index a9c461e..890f6e8 100644 --- a/deploy/helm/values.yaml +++ b/deploy/helm/values.yaml @@ -50,6 +50,14 @@ ingress: hpa: enabled: false +# This section is for setting up autoscaling more information can be found here: https://kubernetes.io/docs/concepts/workloads/autoscaling/ +autoscaling: + enabled: false + minReplicas: 1 + maxReplicas: 100 + targetCPUUtilizationPercentage: 80 + # targetMemoryUtilizationPercentage: 80 + # Environment variables to inject into the container # Example configuration for SLM server settings env: {} @@ -73,7 +81,7 @@ env: {} # See https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ resources: limits: - cpu: 2.5 + cpu: 1500m memory: 800Mi # requests: # cpu: 1 @@ -85,7 +93,7 @@ probes: enabled: true path: /health initialDelaySeconds: 10 - periodSeconds: 10 + periodSeconds: 70 timeoutSeconds: 5 successThreshold: 1 failureThreshold: 3 @@ -93,7 +101,7 @@ probes: enabled: true path: /health initialDelaySeconds: 30 - periodSeconds: 30 + periodSeconds: 70 timeoutSeconds: 5 successThreshold: 1 failureThreshold: 3 diff --git a/docs/2025-05-06-1010-slm.excalidraw b/docs/2025-05-06-1010-slm.excalidraw new file mode 100644 index 0000000..3c4ad93 --- /dev/null +++ b/docs/2025-05-06-1010-slm.excalidraw @@ -0,0 +1,736 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "d5KFGQtTqgmEfUQ2f9Nui", + "type": "image", + "x": 819.1111111111111, + "y": 338.55555555555554, + "width": 50, + "height": 50, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a0", + "roundness": null, + "seed": 1050387154, + "version": 153, + "versionNonce": 2093715790, + "isDeleted": false, + "boundElements": null, + "updated": 1753067882970, + "link": null, + "locked": false, + "status": "saved", + "fileId": "fe765207c7c2adca59a2173fbf9c725e4d5e8755", + "scale": [ + 1, + 1 + ], + "crop": null + }, + { + "id": "iWDIL1-MFEBmwZ9qkPOuu", + "type": "image", + "x": 887.3333333333333, + "y": 336.7777777777777, + "width": 58, + "height": 58, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a1", + "roundness": null, + "seed": 265819474, + "version": 114, + "versionNonce": 807942030, + "isDeleted": false, + "boundElements": null, + "updated": 1753067882970, + "link": null, + "locked": false, + "status": "saved", + "fileId": "0eb3377779b69ac16bde4fd0d2567e364b814c2a", + "scale": [ + 1, + 1 + ], + "crop": null + }, + { + "id": "f93nRt5J3nf1Q8Rup5ZtG", + "type": "image", + "x": 814.5555555555555, + "y": 220.66666666666674, + "width": 48, + "height": 48, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a2", + "roundness": null, + "seed": 483176014, + "version": 191, + "versionNonce": 362157518, + "isDeleted": false, + "boundElements": [ + { + "id": "DJgZwmqfHEsLX1iv1EqyA", + "type": "arrow" + }, + { + "id": "yBaZcwB1LF4TS-iScZHP0", + "type": "arrow" + } + ], + "updated": 1753067882970, + "link": null, + "locked": false, + "status": "saved", + "fileId": "f35a28da51b3598320e399118f525c8c132d8e45", + "scale": [ + 1, + 1 + ], + "crop": null + }, + { + "id": "Y_sGogfPepqR6CRfkbdcb", + "type": "image", + "x": 970.1111111111111, + "y": 344.0000000000001, + "width": 48, + "height": 48, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a3", + "roundness": null, + "seed": 2094341070, + "version": 155, + "versionNonce": 429981838, + "isDeleted": false, + "boundElements": null, + "updated": 1753067882970, + "link": null, + "locked": false, + "status": "saved", + "fileId": "92a85fbc42d60fcac05944e0037af7644c690d70", + "scale": [ + 1, + 1 + ], + "crop": null + }, + { + "id": "7zYKfz8LVzN-YZVTuPswr", + "type": "image", + "x": 564.6666666666667, + "y": 216.33333333333334, + "width": 50, + "height": 50, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a5", + "roundness": null, + "seed": 1484408462, + "version": 154, + "versionNonce": 1349612434, + "isDeleted": false, + "boundElements": [ + { + "id": "DJgZwmqfHEsLX1iv1EqyA", + "type": "arrow" + } + ], + "updated": 1753067792330, + "link": null, + "locked": false, + "status": "saved", + "fileId": "bc6b89be43245e2a153c036535ad8e9913543d77", + "scale": [ + 1, + 1 + ], + "crop": null + }, + { + "id": "0aCxLnwtDDtxnqRyN-XLQ", + "type": "image", + "x": 1042.444444444444, + "y": 224.11111111111111, + "width": 50, + "height": 50, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a6", + "roundness": null, + "seed": 2099508878, + "version": 274, + "versionNonce": 1718104402, + "isDeleted": false, + "boundElements": [ + { + "id": "yBaZcwB1LF4TS-iScZHP0", + "type": "arrow" + } + ], + "updated": 1753067888299, + "link": null, + "locked": false, + "status": "saved", + "fileId": "ca62941f7399e66463568aa8918322939c388b38", + "scale": [ + 1, + 1 + ], + "crop": null + }, + { + "id": "-fbJNM47uFdykrFvSeyZK", + "type": "text", + "x": 716.3333333333333, + "y": 119.66666666666666, + "width": 184.7999267578125, + "height": 35, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a7", + "roundness": null, + "seed": 2027839762, + "version": 81, + "versionNonce": 519133134, + "isDeleted": false, + "boundElements": null, + "updated": 1753067749346, + "link": null, + "locked": false, + "text": "Architecture", + "fontSize": 28, + "fontFamily": 8, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Architecture", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "DJgZwmqfHEsLX1iv1EqyA", + "type": "arrow", + "x": 624.111111111111, + "y": 244.0962194762647, + "width": 181.1111111111113, + "height": 0.5774534760355721, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a9", + "roundness": { + "type": 2 + }, + "seed": 805840974, + "version": 115, + "versionNonce": 1177431054, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "YvRJPW3RMyvdjxCzLJwYK" + } + ], + "updated": 1753067882970, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 181.1111111111113, + 0.5774534760355721 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "7zYKfz8LVzN-YZVTuPswr", + "focus": 0.10560652395514694, + "gap": 9.444444444444343 + }, + "endBinding": { + "elementId": "f93nRt5J3nf1Q8Rup5ZtG", + "focus": -0.003494034616608133, + "gap": 9.333333333333258 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "YvRJPW3RMyvdjxCzLJwYK", + "type": "text", + "x": 634.6111111111112, + "y": 232.7779785303318, + "width": 99, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a9V", + "roundness": null, + "seed": 591776078, + "version": 27, + "versionNonce": 1827609102, + "isDeleted": false, + "boundElements": null, + "updated": 1753067840333, + "link": null, + "locked": false, + "text": "/chat/...", + "fontSize": 20, + "fontFamily": 8, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "DJgZwmqfHEsLX1iv1EqyA", + "originalText": "/chat/...", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "Q71UemsDoPUZGUU7GmF7M", + "type": "text", + "x": 559.6666666666666, + "y": 285.2222222222223, + "width": 66, + "height": 25, + "angle": 0.008391411423452233, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aA", + "roundness": null, + "seed": 2093717522, + "version": 30, + "versionNonce": 664588306, + "isDeleted": false, + "boundElements": null, + "updated": 1753067791121, + "link": null, + "locked": false, + "text": "Client", + "fontSize": 20, + "fontFamily": 8, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Client", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "eHqg04c1unPSY-TnKNTyF", + "type": "text", + "x": 802.2220285799476, + "y": 279.9905966656282, + "width": 77, + "height": 25, + "angle": 0.008391411423452233, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aB", + "roundness": null, + "seed": 364518034, + "version": 116, + "versionNonce": 1682629454, + "isDeleted": false, + "boundElements": [], + "updated": 1753067882970, + "link": null, + "locked": false, + "text": "fastapi", + "fontSize": 20, + "fontFamily": 8, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "fastapi", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "ClsA_P3JNbttcvXpm6Tl5", + "type": "text", + "x": 840.7777777777778, + "y": 404.66666666666663, + "width": 176, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aC", + "roundness": null, + "seed": 868887438, + "version": 73, + "versionNonce": 1340950926, + "isDeleted": false, + "boundElements": null, + "updated": 1753067882970, + "link": null, + "locked": false, + "text": "Monitoring Stack", + "fontSize": 20, + "fontFamily": 8, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Monitoring Stack", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "yBaZcwB1LF4TS-iScZHP0", + "type": "arrow", + "x": 872.9999999999999, + "y": 244.68951023155142, + "width": 156.6666666666664, + "height": 1.2735691422958553, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aD", + "roundness": { + "type": 2 + }, + "seed": 655191246, + "version": 210, + "versionNonce": 683292434, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "Z_b8UlxbEyarx5eKiCAqs" + } + ], + "updated": 1753067888300, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 156.6666666666664, + -1.2735691422958553 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "f93nRt5J3nf1Q8Rup5ZtG", + "focus": 0.0041498785598879095, + "gap": 10.444444444444343 + }, + "endBinding": { + "elementId": "0aCxLnwtDDtxnqRyN-XLQ", + "focus": 0.23815775589217766, + "gap": 12.777777777777601 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "Z_b8UlxbEyarx5eKiCAqs", + "type": "text", + "x": 809.6666666666666, + "y": 233.1247267891514, + "width": 110, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aE", + "roundness": null, + "seed": 1900016654, + "version": 32, + "versionNonce": 1672004878, + "isDeleted": false, + "boundElements": null, + "updated": 1753067875591, + "link": null, + "locked": false, + "text": "llama...()", + "fontSize": 20, + "fontFamily": 8, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "yBaZcwB1LF4TS-iScZHP0", + "originalText": "llama...()", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "yu01Z2_snG4HGUOe-RjlZ", + "type": "text", + "x": 1039.6666666666665, + "y": 284.66666666666663, + "width": 253, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aG", + "roundness": null, + "seed": 1633567122, + "version": 47, + "versionNonce": 170291214, + "isDeleted": false, + "boundElements": null, + "updated": 1753067911951, + "link": null, + "locked": false, + "text": "llama-cpp + local model", + "fontSize": 20, + "fontFamily": 8, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "llama-cpp + local model", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "izB3JSpU79Wxs336BTdte", + "type": "rectangle", + "x": 776.3333333333333, + "y": 189.11111111111111, + "width": 545.5555555555559, + "height": 277.7777777777779, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aH", + "roundness": { + "type": 3 + }, + "seed": 1285985106, + "version": 255, + "versionNonce": 1667324878, + "isDeleted": false, + "boundElements": null, + "updated": 1753067933988, + "link": null, + "locked": false + }, + { + "id": "JEnNdpxXvZeQeeseWxlvh", + "type": "image", + "x": 1162.4444444444443, + "y": 224.1111111111111, + "width": 44.4444444444444, + "height": 44.4444444444444, + "angle": 0, + "strokeColor": "transparent", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aJ", + "roundness": null, + "seed": 225450126, + "version": 123, + "versionNonce": 765751182, + "isDeleted": false, + "boundElements": null, + "updated": 1753067970475, + "link": null, + "locked": false, + "status": "saved", + "fileId": "e848d67084586251f2c893c65804900d56af1f0d", + "scale": [ + 1, + 1 + ], + "crop": null + } + ], + "appState": { + "gridSize": 20, + "gridStep": 5, + "gridModeEnabled": false, + "viewBackgroundColor": "#ffffff", + "lockedMultiSelections": {} + }, + "files": { + "fe765207c7c2adca59a2173fbf9c725e4d5e8755": { + "mimeType": "image/png", + "id": "fe765207c7c2adca59a2173fbf9c725e4d5e8755", + "dataURL": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAAXNSR0IArs4c6QAAAxNJREFUaEPtmFnITVEUx39fksxTppLMU7wplKGMUSJkePCVQt49k3hRFK9IXuTFlKEQ4kWSlELmTCEiJTJk2v9at063e7999j332MfX2U93WGef9VvD3mutFtrJamknHPhA/uQEegZYDnxv1v6xQKT/WWAp8K0ZMGlBfHIhuiS9fAVYBHwO2aCWrE/Bykt9ciF6VPZ8CwwALhvMl5BNqmV9CuYJMha4CAwGrgILgU+NwsQE0buHApeA4c4rN4H5wIdGYGKDSOchBjMSuOW8Mxd4HwpTBBDpPNBgxgP3gNnAmxCYooBIZyX+BWAi8MBgXqWFKRKIdO4DnAcmAc+AWcDTNDBFA5HOvYBz7sKcDLwwmCc+mCKCSOeedvNPBV5amD1qC6aoINK5K3DKPKLLc46rz+7Ug4kJ4ouW6v/f2YFQ87n/CUQAdfWNARLqCcl7S6USpBGzZnim9EjFeF5LZLByyKNePcocMXOOcaX3EuCuq480UEiuQUAr0Bk4YjLJ/1VXqWdXT/IaOAh8reOm3D2yDDgKHALWJJSQcteB/vabBgwqAK/Z95nACaC3ff8B9GhjqhINZD+wDthhU5KtVggusAr3IdAXOODK9mNAN/NavbyJAtLROrwuVsn+sl5cyiic1ruicLf16+oG06woIGpdn1sfoV5cS2W4Po8DtttwbiOwNw3Fv7jZa+WImqIbVqmq29O67XJpgvUYu1xbO91GQGpxFYZamjwqzGqtKB4Zba3qY2CUaaXPI4BhlhdK/JV2WinMBF84kO7ARzuB1CDJmpokdrKjeA+gsFKIbQE2A9uKCCInaFYlq68AfgLH7TieAsyzvlzN0jTnvdVFAknGteJffbe6Oy15RBXEYuC0fT4MrHIe+W2e06UZNbQ0INhUlZ0Kl/t2428wkH3AyYRcB3eyrQVmAP1syLATUC5FSfaUp2dmsdxPrcwaptygBKkYymuJlBbNKubVo+xHspo48PnSI2WOBIZMWvEytMrQShsrgXJNC63A9+Ymnnkan5tmgRs3DBL4nnjivhIlnmaBb/4LcnPZM8mRV7cAAAAASUVORK5CYII=", + "created": 1753067502326, + "lastRetrieved": 1753067502326 + }, + "0eb3377779b69ac16bde4fd0d2567e364b814c2a": { + "mimeType": "image/png", + "id": "0eb3377779b69ac16bde4fd0d2567e364b814c2a", + "dataURL": "data:image/png;base64,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", + "created": 1753067506989, + "lastRetrieved": 1753067506989 + }, + "f35a28da51b3598320e399118f525c8c132d8e45": { + "mimeType": "image/png", + "id": "f35a28da51b3598320e399118f525c8c132d8e45", + "dataURL": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAABJFJREFUaEPtmX9o1HUYx1/P585tnQMDI8iIRVlBLhEyrLDdbdMNC0YUCoIUaegfkSy0u6OwhkZu01JJKLD1R38s6p9RWmzs9tMiSP+Jin4b/bEMqah0ebfdfZ68zZVtd/t+7r6TOdrn7/fzfN6v5/l+7vN8vyfM8SVz3D/zALPdwfkOjHdAhXhihcHUqXK3oLegLAFZiGgG+FXQIRVzwkBHuux4P01Ndia6578DOxPLTYB2kGWuhhTt0nRgEy9V/+Iak0/nD6DpaMicD30DXF+oEVE+zoSOr/bbCV8AwVhvvUU7CzU/oTei69LNa4qOz+bxBRCI925W1TZgyBqtw+o5gzkG3OEEJbrHNq95zkmbR+QLwER7GhEOgLTZlprHs3uYaO8uRHc7mjpoW2qfctTmlM0QwMUOjMiwCXLUuQNwxQAUW8R5AF+PkEvZ/z0nOdVjHdDB0kY0e5b+WUmQz1C7VyIjHdPtc6UCXLzgUYxslark68VdZPG+pUZ1J9jVIDcCC12qXoAmXwcuTXGOtFRKbfLHXHnzdiAQ7W1Q9C2EUAGGCpW6AGRHrb0SST7jDrCjp8IE+eIyVHySB3nZttTsyHEG/qtTPSWRkZudAUw8cQiV7YWWs2C98qRtrT2sAyXPgrwwbbwNXifVwz9P1uR8hEy0+1vELC3YUEEB+rvNZG5jf/0ZHSh7G3SDR/gDEk594A3Q2He1Kc38BnI5f6GGDfJwuqWmSz8qv5b06PdAuQdAo4RTh7wB4t0Ro6avoGK6iBULMgTaZZVm9tVmTaODZe2obvRMIbJbqpLPewIE4j0bVbMvKK5LvxbMixlNJ2hdexpEXSJVMQyU7kdwG+aUfRJJRT0BTCzxBMhhFxNAu/0rtZlX7k856hl/ZEYiqIkieqdrHGhcwiMtngCBaGKLiuS9+SYSKPTpVaaOpuq0fhhaQiYTA224MFrfAATcjbkq5VEJJ990AXhQRaadP1CsxVbSuvZL7VuwEiOdIItdrRSlM3aF3Df6qScAsd5lBv18uk3G3mdba+/VPsoIlHyFSkVRplyDlPOUpxbJSka9Ada/EzA3Lf7D4xY+Yltqt+pAaQPwrqsPH7oOCaceyhWf87denk70iJGaaTYcn2H6S7cjTPlt9mE0V2gaa++R6tGTzgAmltgG8ponwNQ5foa9kwbZJuHkG/kS575tY92LDCY7zOX73uM2RRaHkwR+wOonBPVAroN7adr843SsZ52qvodIMIcPF4CDEk65XVLFgY5FTT/vxHqqLgiaRFk16b1gjgB4VMZzjveurO8u+Zo4/2cAOoRQhwbOovYYwnJgDnXA0ibVqbHPjzpYsguV7OfHWQYYKNsC6jn4jR0F1Z9Q6gmaP7H6PlCJyh6JJGfv464OltyOSva+KG6pbJVI8khxweNRvg7xWGHd3menelTOosFbc72oFwLkH+AkIYZLXgV5xHlj1VOIeUzCyUHnmDxC3wATebV/wV1g1gOrMFqByjVAKSpnQc8gfAd6AhPo5PT5k7KB7J9/vteMAfh2UmSCeYAiCzdjYfMdmLFSFpnob5NY4UBlvFgCAAAAAElFTkSuQmCC", + "created": 1753067523854, + "lastRetrieved": 1753067523854 + }, + "92a85fbc42d60fcac05944e0037af7644c690d70": { + "mimeType": "image/png", + "id": "92a85fbc42d60fcac05944e0037af7644c690d70", + "dataURL": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAAAXNSR0IArs4c6QAAAqJJREFUaEPtWE1rU0EUPXcSXwqatlRE6caFtGBNBNGlinRRKNj6keQvVBCTCmqbRBehYFJEEF79ByKIefoHXIp2IVUQu3BjF11bbBfFNLy5kmClaK0zb8bW4rz1vffcc86dd+c9wi5/aJf3D0dgpx10DjgHDBVwI2QooHG6c8BYQsMC/68D2ULxlWQhn89UzxiKqJy+GeYPB0YnJpJeI34JIQ1ChH1gHADEIQYnSOALQlqAwGuCfFz3p+cu58svBYEDv3pWuYOfAm1gtghQrlCaZKYyCEmVZgh4hpg3Vn9QWYrohDVMyubL90G4odL4xhgJzHWueKdXkmsvdJ2wiUmZQmmZQJ26BL7HlwO/WtPNtYlJmUL5IwH9uk2045nng5laSneMbGJSrlA8CRmbksQDAPcSkadOhlcDv7ZX90DbxPztHsiNTXahI55mxjAQ5kCi7xdiEp+Ch9Uj6oS3joyCqbzIMvniCQEagaChkDEgiJtgGg/86hPdEVIlrIKpTGArUN0RUiWggmmFgI2GotZwBKIqZyvPOWBLyah1nANRlWvlVSoV8eHz2juQXAz86fNRam2LA62FRCRGATkkIY6uL8FUj/d0fqn5loHFwL878s8QuHi90h0PG2kQDYNl9m9eQ4wcyFy7fYpi4RSYBhjUS8AedRV5NdWTSO7oCJlei1P7E8d3dIRMPkwIXKr7tWl1xzaPNBuh8eI9YnFLtwlivPna8M4lEmuzJm+gFq4RgfYPgXz5piS+o/FZWvea8kr/wY5l0/GxQaAtfu5qZR/HmhdA4SCxSLPgw2DqYmYCiSVmXiDiWSHo0bFu773pwd3ouKkDutPTXl42lF8H3nYC2oz/kOAI2FZUt55zQFcx2/HOAduK6tZzDugqZjt+1zvwDXI4bTRIIOWKAAAAAElFTkSuQmCC", + "created": 1753067531166, + "lastRetrieved": 1753067531166 + }, + "bc6b89be43245e2a153c036535ad8e9913543d77": { + "mimeType": "image/png", + "id": "bc6b89be43245e2a153c036535ad8e9913543d77", + "dataURL": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAAXNSR0IArs4c6QAAA/pJREFUaEPtmVmsjkcYx38HsZWI4EZE7FvsXPaioRWt1BpLLDeERIhdEdGiERQNrSIuuEEsscZSQRMXLotKJYhWKiUi0UiQam2df/ocOZzznXfemTmOnHyTnOTLe+Z55v9/5pl5limhhoySGsKDIpFKdrIuMAIYDvQBWtncP4GL7ttR4DDwb0pvSL0jo4G1QNsMkL8DXwAHU5FJRaQ2sA6Ya8AuATuAc8Bt+9YaGAhMAXrbtw3AIuBFLKFURL41Ek+BGcBO4FUBcLUcucnA90B9QGQWvA9E5E77nWX/Bj5xfxc8QX0InDEyo4BDnnIVTovdER3sa3Ym5DJypzxjKrAd+M0Zo1vMBRBLZCywF9CZ6A+8zMMCkJvpJusFjAEO5JR/PT2WiEiIzEzgh0AQs4BNwB5gQqCO6IB43QHoBHQ1FwvBIZe6CkhXlxAFkondkUdAI3fzNHY3z+NAEJKXHslLT9CIJVJ6xVa7nmoHYOaPNkiRSCpLptJT3JFUlkylJ3ZHHgJNXEBrCuh3yJDsXyav30EjlsgVV1P0APpamhICQrI/u0LsshViITqiA+JuYDyw0AXG9UEI/pf9BtjlksdJgTqiiXwKnHRkbliaEpI0KnvuCEjXj9VFRJWhcqT2wGIrc/Ngkcxq4KblWcGVYuwZEeiPgJ+AZ+6cDMth1cHubB0DZIwBwPk8Fnh7bgoi0rnS1d3LgOfuBlJavjUD1HTgO6COyX4VQyJF9lt2fTUR1tiHLAOV5lbquMi9okfWgnkX8E3+fOd5r18kUsBUvpb2nVfcEW8LVDCxBXDfHV416RpkKFIPTM255sCDmEVLZVOdEcUEXbltXFvouMudPs8AdwL4zAKhOjCnY8nEEFFzTr2oOUA/A/ILMAS4kwFMfWClI+q+aChp3Ggdy6AufQiRZsA062W1NCD3LGncDPzjad2GwDxgtrmYxO6639Kh7mMul8tDRDugiLzCahAtrGRxiy0svw8Z9azJpyy4uylQa0jNbQVML72+RAYB28q8e5y1ZwQ1oQt13fOSEhato878xyasJwkZTxl2pcOHyFLLh9Sn/dUWij6cGbjU1Vd909MMpVxueWUyWUQkrIRO6bWUrUrxKJNlXfu/DKc8TK6s5FJ53JJCspURGepelo4Y8InuhtnnCSD1tJHW8RcZvaPo/bHcKEREB1sFk+JCSMGUmsx8c7U/rGle7oouRETtfdXQt4DOVjSlBpdHn3ZDhm1ndb2wvTEKEZEbKdh97R5vvsyzYhXOLS3ehG2cLxE9hYm9Uo+qvqF8uQvLKXum6+BLRAHpA98V3vG8J/Ym4+VaqYJcVXEsdySy4khVAUmut0gkuUkjFdaYHfkP4Hm0M1SzmDQAAAAASUVORK5CYII=", + "created": 1753067711904, + "lastRetrieved": 1753067711904 + }, + "ca62941f7399e66463568aa8918322939c388b38": { + "mimeType": "image/png", + "id": "ca62941f7399e66463568aa8918322939c388b38", + "dataURL": "data:image/png;base64,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", + "created": 1753067735200, + "lastRetrieved": 1753067735200 + }, + "e848d67084586251f2c893c65804900d56af1f0d": { + "mimeType": "image/png", + "id": "e848d67084586251f2c893c65804900d56af1f0d", + "dataURL": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAAAyCAYAAAAeP4ixAAAAAXNSR0IArs4c6QAAARFJREFUaEPtmeEJwkAMhV+X0BVEp9DBXUNxAHGLSpCCCGcul4Q76+uf/skl+ZKXXqATVvJMK+GABjIngl4AnAA8ImL0BJH8bwCOAO5emFoQzc6ax3unQ2C0BJeAml0ryBXADoC8pTPNMtMSzAbZAjgDOHhl1htE4m8iYEYAEVm6YUYBccOMBOKCGQ2kGWZEkCaYUUHMML1BLBep7Gb70oFfAhGGYr69QCydEFt1wyCItaROe3ZkKaBaCWela4+reXBGaksZZMeOcEaCpPTphtKitCit7xXgjHBGOCOckVcFuP0mzULJLT+//PwmSY7SorQoLd7sf3qzJ0nf7Nb9W8EcMelAM0hSPvFute03PmKSxyeLw3ozZCg7nQAAAABJRU5ErkJggg==", + "created": 1753067962323, + "lastRetrieved": 1753067962323 + } + } +} \ No newline at end of file diff --git a/docs/20250712_slm_img1.jpg b/docs/20250712_slm_img1.jpg new file mode 100644 index 0000000..df79bcb Binary files /dev/null and b/docs/20250712_slm_img1.jpg differ